diff --git a/.gitignore b/.gitignore index 0385ff2..9491a2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,21 @@ -*.swp -*.*~ -project.lock.json -.DS_Store -*.pyc -nupkg/ - -# Visual Studio Code -.vscode +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files +*.rsuser *.suo *.user *.userosscache *.sln.docstates +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + # Build results [Dd]ebug/ [Dd]ebugPublic/ @@ -21,11 +23,341 @@ nupkg/ [Rr]eleases/ x64/ x86/ -build/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Oo]ut/ -msbuild.log -msbuild.err -msbuild.wrn +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/insight.sln b/insight.sln new file mode 100644 index 0000000..c6e0266 --- /dev/null +++ b/insight.sln @@ -0,0 +1,105 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32210.238 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{88B03853-2215-4E52-8986-0E76602E5F21}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{038C3821-E554-496D-B585-A3BC193B7913}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Agent", "Agent", "{140F73DD-29D3-4C44-809B-5B470880AA0D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Setup", "Setup", "{15D04093-4974-4B2F-AE8A-F3721F31767A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Updater", "Updater", "{F2D241DB-7692-46DB-8A6A-958B365DAAF8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Insight.Infrastructure", "src\Core\Insight.Infrastructure\Insight.Infrastructure.csproj", "{3DC54216-3D5A-4DCE-9B1E-3D1AF2DE8C2C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{3F000016-069D-477E-ACA3-F643880B57E8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Insight.Domain", "src\Core\Insight.Domain\Insight.Domain.csproj", "{02A50CD8-40DF-4329-89A9-961ED78EDAA6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Insight.Setup.Windows", "src\Setup\Insight.Setup.Windows\Insight.Setup.Windows.csproj", "{CA99B8CF-520A-4B48-ACCE-0A301C35A7FE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Insight.Web", "src\Web\Insight.Web\Insight.Web.csproj", "{375EF474-512A-4410-86CF-46974E07C1F7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Api", "Api", "{35BA5DCB-BECC-4F51-8DD0-694C555D205A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Insight.Api", "src\Api\Insight.Api\Insight.Api.csproj", "{EF3188D7-338D-43DA-BF6B-D26E5BDAC3A6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Insight.Server", "src\Server\Insight.Server\Insight.Server.csproj", "{1E75F7E9-E6AA-44E7-A2F3-DB4CA85E0118}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Insight.Updater", "src\Updater\Insight.Updater\Insight.Updater.csproj", "{4875D70F-A96B-4EBA-99BE-218886D29BEB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Insight.Web.Assets", "src\Web\Insight.Web.Assets\Insight.Web.Assets.csproj", "{EBB8A2A8-453B-4867-A8E2-072530391DD0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Insight.Agent", "src\Agent\Insight.Agent\Insight.Agent.csproj", "{2A391CA2-F96B-4DB7-80AA-0668A5141640}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Insight.Agent.Assets", "src\Agent\Insight.Agent.Assets\Insight.Agent.Assets.csproj", "{4C2B66EA-4EE1-47BF-BAEE-DDBAF6FCB324}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3DC54216-3D5A-4DCE-9B1E-3D1AF2DE8C2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DC54216-3D5A-4DCE-9B1E-3D1AF2DE8C2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DC54216-3D5A-4DCE-9B1E-3D1AF2DE8C2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DC54216-3D5A-4DCE-9B1E-3D1AF2DE8C2C}.Release|Any CPU.Build.0 = Release|Any CPU + {02A50CD8-40DF-4329-89A9-961ED78EDAA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02A50CD8-40DF-4329-89A9-961ED78EDAA6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02A50CD8-40DF-4329-89A9-961ED78EDAA6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02A50CD8-40DF-4329-89A9-961ED78EDAA6}.Release|Any CPU.Build.0 = Release|Any CPU + {CA99B8CF-520A-4B48-ACCE-0A301C35A7FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA99B8CF-520A-4B48-ACCE-0A301C35A7FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA99B8CF-520A-4B48-ACCE-0A301C35A7FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA99B8CF-520A-4B48-ACCE-0A301C35A7FE}.Release|Any CPU.Build.0 = Release|Any CPU + {375EF474-512A-4410-86CF-46974E07C1F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {375EF474-512A-4410-86CF-46974E07C1F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {375EF474-512A-4410-86CF-46974E07C1F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {375EF474-512A-4410-86CF-46974E07C1F7}.Release|Any CPU.Build.0 = Release|Any CPU + {EF3188D7-338D-43DA-BF6B-D26E5BDAC3A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF3188D7-338D-43DA-BF6B-D26E5BDAC3A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF3188D7-338D-43DA-BF6B-D26E5BDAC3A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF3188D7-338D-43DA-BF6B-D26E5BDAC3A6}.Release|Any CPU.Build.0 = Release|Any CPU + {1E75F7E9-E6AA-44E7-A2F3-DB4CA85E0118}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E75F7E9-E6AA-44E7-A2F3-DB4CA85E0118}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E75F7E9-E6AA-44E7-A2F3-DB4CA85E0118}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E75F7E9-E6AA-44E7-A2F3-DB4CA85E0118}.Release|Any CPU.Build.0 = Release|Any CPU + {4875D70F-A96B-4EBA-99BE-218886D29BEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4875D70F-A96B-4EBA-99BE-218886D29BEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4875D70F-A96B-4EBA-99BE-218886D29BEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4875D70F-A96B-4EBA-99BE-218886D29BEB}.Release|Any CPU.Build.0 = Release|Any CPU + {EBB8A2A8-453B-4867-A8E2-072530391DD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBB8A2A8-453B-4867-A8E2-072530391DD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBB8A2A8-453B-4867-A8E2-072530391DD0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBB8A2A8-453B-4867-A8E2-072530391DD0}.Release|Any CPU.Build.0 = Release|Any CPU + {2A391CA2-F96B-4DB7-80AA-0668A5141640}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A391CA2-F96B-4DB7-80AA-0668A5141640}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A391CA2-F96B-4DB7-80AA-0668A5141640}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A391CA2-F96B-4DB7-80AA-0668A5141640}.Release|Any CPU.Build.0 = Release|Any CPU + {4C2B66EA-4EE1-47BF-BAEE-DDBAF6FCB324}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C2B66EA-4EE1-47BF-BAEE-DDBAF6FCB324}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C2B66EA-4EE1-47BF-BAEE-DDBAF6FCB324}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C2B66EA-4EE1-47BF-BAEE-DDBAF6FCB324}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {3DC54216-3D5A-4DCE-9B1E-3D1AF2DE8C2C} = {88B03853-2215-4E52-8986-0E76602E5F21} + {02A50CD8-40DF-4329-89A9-961ED78EDAA6} = {88B03853-2215-4E52-8986-0E76602E5F21} + {CA99B8CF-520A-4B48-ACCE-0A301C35A7FE} = {15D04093-4974-4B2F-AE8A-F3721F31767A} + {375EF474-512A-4410-86CF-46974E07C1F7} = {3F000016-069D-477E-ACA3-F643880B57E8} + {EF3188D7-338D-43DA-BF6B-D26E5BDAC3A6} = {35BA5DCB-BECC-4F51-8DD0-694C555D205A} + {1E75F7E9-E6AA-44E7-A2F3-DB4CA85E0118} = {038C3821-E554-496D-B585-A3BC193B7913} + {4875D70F-A96B-4EBA-99BE-218886D29BEB} = {F2D241DB-7692-46DB-8A6A-958B365DAAF8} + {EBB8A2A8-453B-4867-A8E2-072530391DD0} = {3F000016-069D-477E-ACA3-F643880B57E8} + {2A391CA2-F96B-4DB7-80AA-0668A5141640} = {140F73DD-29D3-4C44-809B-5B470880AA0D} + {4C2B66EA-4EE1-47BF-BAEE-DDBAF6FCB324} = {140F73DD-29D3-4C44-809B-5B470880AA0D} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F376A326-7590-4E7E-AB9B-76CED8527AB0} + EndGlobalSection +EndGlobal diff --git a/src/Agent/Insight.Agent.Assets/Enums/CategoryEnum.cs b/src/Agent/Insight.Agent.Assets/Enums/CategoryEnum.cs new file mode 100644 index 0000000..5ba8cb9 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Enums/CategoryEnum.cs @@ -0,0 +1,14 @@ +namespace Insight.Agent.Enums +{ + public enum CategoryEnum + { + Network = 1, + System = 2, + Application = 3, + Security = 4, + Monitoring = 5, + Task = 6, + Printer = 7, + RDP = 8 + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Enums/DispatchEnum.cs b/src/Agent/Insight.Agent.Assets/Enums/DispatchEnum.cs new file mode 100644 index 0000000..2d824d4 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Enums/DispatchEnum.cs @@ -0,0 +1,9 @@ +namespace Insight.Agent.Enums +{ + public enum DispatchEnum + { + Pending = 1, + Failure = 2, + Success = 3, + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Enums/StatusEnum.cs b/src/Agent/Insight.Agent.Assets/Enums/StatusEnum.cs new file mode 100644 index 0000000..49b8e7d --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Enums/StatusEnum.cs @@ -0,0 +1,9 @@ +namespace Insight.Agent.Enums +{ + public enum StatusEnum + { + Information = 1, + Warning = 2, + Error = 3 + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Insight.Agent.Assets.csproj b/src/Agent/Insight.Agent.Assets/Insight.Agent.Assets.csproj new file mode 100644 index 0000000..540ca66 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Insight.Agent.Assets.csproj @@ -0,0 +1,30 @@ + + + + net7.0 + true + enable + Insight.Agent.Assets + Insight.Agent + Insight + 2023.9.14.0 + + + + none + + + + none + + + + + + + + + + + + diff --git a/src/Agent/Insight.Agent.Assets/Interfaces/IAgentMessageHandler.cs b/src/Agent/Insight.Agent.Assets/Interfaces/IAgentMessageHandler.cs new file mode 100644 index 0000000..a9e69a6 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Interfaces/IAgentMessageHandler.cs @@ -0,0 +1,9 @@ +using Insight.Agent.Messages; + +namespace Insight.Agent.Interfaces +{ + public partial interface IAgentMessageHandler + { + ValueTask HandleAsync(TSender sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage; + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Application/Application.cs b/src/Agent/Insight.Agent.Assets/Messages/Application/Application.cs new file mode 100644 index 0000000..ba3ad5c --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Application/Application.cs @@ -0,0 +1,41 @@ +using MemoryPack; +using System.Runtime.InteropServices; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(120, typeof(Application))] + [MemoryPackUnion(121, typeof(ApplicationList))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Application : IAgentMessage + { + [MemoryPackOrder(0)] + public string? Name { get; set; } + + [MemoryPackOrder(1)] + public string? Publisher { get; set; } + + [MemoryPackOrder(2)] + public string? Version { get; set; } + + [MemoryPackOrder(3)] + public string? Location { get; set; } + + [MemoryPackOrder(4)] + public string? Source { get; set; } + + [MemoryPackOrder(5)] + public string? Uninstall { get; set; } + + [MemoryPackOrder(6)] + public DateTime? InstallDate { get; set; } + + [MemoryPackOrder(7)] + public Architecture? Architecture { get; set; } + } + + [MemoryPackable(GenerateType.Collection)] + public partial class ApplicationList : List, IAgentMessage { } + +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Authentication/Authentication.cs b/src/Agent/Insight.Agent.Assets/Messages/Authentication/Authentication.cs new file mode 100644 index 0000000..84406f4 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Authentication/Authentication.cs @@ -0,0 +1,34 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(1, typeof(Authentication))] + [MemoryPackUnion(2, typeof(AuthenticationRequest))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Authentication : IAgentMessage + { + [MemoryPackOrder(0)] + public PlatformType? Platform { get; set; } + + [MemoryPackOrder(1)] + public Guid Serial { get; set; } + + [MemoryPackOrder(2)] + public Version? Version { get; set; } + + [MemoryPackOrder(3)] + public string? Hostname { get; set; } + + public enum PlatformType + { + Unknown = 0, + Windows = 1, + Unix = 2 + } + } + + [MemoryPackable] + public partial class AuthenticationRequest : IAgentMessage { } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Commands/Commands.cs b/src/Agent/Insight.Agent.Assets/Messages/Commands/Commands.cs new file mode 100644 index 0000000..b231232 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Commands/Commands.cs @@ -0,0 +1,10 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(2000, typeof(GetInventory))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class GetInventory : IAgentMessage { } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Console/ConsoleQuery.cs b/src/Agent/Insight.Agent.Assets/Messages/Console/ConsoleQuery.cs new file mode 100644 index 0000000..369304c --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Console/ConsoleQuery.cs @@ -0,0 +1,49 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(1000, typeof(ConsoleQuery))] + [MemoryPackUnion(1001, typeof(ConsoleQueryRequest))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class ConsoleQuery : IAgentMessage + { + [MemoryPackOrder(0)] + public string? Id { get; set; } + + [MemoryPackOrder(1)] + public string? HostId { get; set; } + + [MemoryPackOrder(2)] + public string? Query { get; set; } + + [MemoryPackOrder(3)] + public string? Data { get; set; } + + [MemoryPackOrder(4)] + public string? Errors { get; set; } + + [MemoryPackOrder(5)] + public bool IsString { get; set; } + + [MemoryPackOrder(6)] + public bool IsArray { get; set; } + + [MemoryPackOrder(7)] + public bool HadErrors { get; set; } + } + + [MemoryPackable] + public partial class ConsoleQueryRequest : IAgentMessage + { + [MemoryPackOrder(0)] + public string? Id { get; set; } + + [MemoryPackOrder(1)] + public string? HostId { get; set; } + + [MemoryPackOrder(2)] + public string? Query { get; set; } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Drive/Drive.cs b/src/Agent/Insight.Agent.Assets/Messages/Drive/Drive.cs new file mode 100644 index 0000000..5a2f966 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Drive/Drive.cs @@ -0,0 +1,103 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(50, typeof(Drive))] + [MemoryPackUnion(51, typeof(DriveList))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Drive : IAgentMessage + { + [MemoryPackOrder(0)] + public uint? Index { get; set; } + + [MemoryPackOrder(1)] + public string? Id { get; set; } + + [MemoryPackOrder(2)] + public string? Name { get; set; } + + [MemoryPackOrder(3)] + public string? Manufacturer { get; set; } + + [MemoryPackOrder(4)] + public string? SerialNumber { get; set; } + + [MemoryPackOrder(5)] + public ulong? Size { get; set; } + + [MemoryPackOrder(6)] + public string? Status { get; set; } + + [MemoryPackOrder(7)] + public string? InterfaceType { get; set; } + + [MemoryPackOrder(8)] + public string? FirmwareRevision { get; set; } + + [MemoryPackOrder(9)] + public string? PNPDeviceID { get; set; } + + [MemoryPackOrder(10)] + public List? Volumes { get; set; } + } + + [MemoryPackable(GenerateType.Collection)] + public partial class DriveList : List, IAgentMessage { } + + [MemoryPackable] + public partial class Volume : IAgentMessage + { + [MemoryPackOrder(0)] + public uint? Index { get; set; } + + [MemoryPackOrder(1)] + public string? Id { get; set; } + + [MemoryPackOrder(2)] + public string? Name { get; set; } + + [MemoryPackOrder(3)] + public string? SerialNumber { get; set; } + + [MemoryPackOrder(4)] + public ulong? Size { get; set; } + + [MemoryPackOrder(5)] + public ulong? FreeSpace { get; set; } + + [MemoryPackOrder(6)] + public string? Type { get; set; } + + [MemoryPackOrder(7)] + public string? FileSystem { get; set; } + + [MemoryPackOrder(8)] + public bool? Compressed { get; set; } + + [MemoryPackOrder(9)] + public bool? Bootable { get; set; } + + [MemoryPackOrder(10)] + public bool? PrimaryPartition { get; set; } + + [MemoryPackOrder(11)] + public bool? BootPartition { get; set; } + + [MemoryPackOrder(12)] + public ulong? BlockSize { get; set; } + + [MemoryPackOrder(13)] + public ulong? NumberOfBlocks { get; set; } + + [MemoryPackOrder(14)] + public ulong? StartingOffset { get; set; } + + [MemoryPackOrder(15)] + public DriveType? DriveType { get; set; } + + [MemoryPackOrder(16)] + public string? ProviderName { get; set; } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Event/Event.cs b/src/Agent/Insight.Agent.Assets/Messages/Event/Event.cs new file mode 100644 index 0000000..edd3b61 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Event/Event.cs @@ -0,0 +1,41 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(10, typeof(Event))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Event : IAgentMessage + { + [MemoryPackOrder(0)] + public DateTime? Timestamp { get; set; } + + [MemoryPackOrder(1)] + public StatusType? Status { get; set; } + + [MemoryPackOrder(2)] + public string? Source { get; set; } + + [MemoryPackOrder(3)] + public string? Category { get; set; } + + [MemoryPackOrder(4)] + public int? EventId { get; set; } + + [MemoryPackOrder(5)] + public string? Task { get; set; } + + [MemoryPackOrder(6)] + public string? Message { get; set; } + + public enum StatusType + { + Unknown = 0, + Information = 1, + Warning = 2, + Error = 3, + Critical = 4 + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/IAgentMessage.cs b/src/Agent/Insight.Agent.Assets/Messages/IAgentMessage.cs new file mode 100644 index 0000000..3c3edf0 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/IAgentMessage.cs @@ -0,0 +1,7 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackable] + public partial interface IAgentMessage { } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Interface/Interface.cs b/src/Agent/Insight.Agent.Assets/Messages/Interface/Interface.cs new file mode 100644 index 0000000..1c008c5 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Interface/Interface.cs @@ -0,0 +1,195 @@ +using MemoryPack; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(70, typeof(Interface))] + [MemoryPackUnion(71, typeof(InterfaceList))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Interface : IAgentMessage + { + [MemoryPackOrder(0)] + public uint? Index { get; set; } + + [MemoryPackOrder(1)] + public Guid? Guid { get; set; } + + [MemoryPackOrder(2)] + public string? Mac { get; set; } + + [MemoryPackOrder(3)] + public string? Name { get; set; } + + [MemoryPackOrder(4)] + public string? Description { get; set; } + + [MemoryPackOrder(5)] + public string? Manufacturer { get; set; } + + [MemoryPackOrder(6)] + public string? Suffix { get; set; } + + [MemoryPackOrder(7)] + public bool? Physical { get; set; } + + [MemoryPackOrder(8)] + public NetworkInterfaceType? Type { get; set; } + + [MemoryPackOrder(9)] + public OperationalStatus? Status { get; set; } + + [MemoryPackOrder(10)] + public long? Speed { get; set; } + + [MemoryPackOrder(11)] + public long? Ipv4Mtu { get; set; } + + [MemoryPackOrder(12)] + public bool? Ipv4Dhcp { get; set; } + + [MemoryPackOrder(13)] + public bool? Ipv4Forwarding { get; set; } + + [MemoryPackOrder(14)] + public long? Ipv6Mtu { get; set; } + + [MemoryPackOrder(15)] + public long? Sent { get; set; } + + [MemoryPackOrder(16)] + public long? Received { get; set; } + + [MemoryPackOrder(17)] + public long? IncomingPacketsDiscarded { get; set; } + + [MemoryPackOrder(18)] + public long? IncomingPacketsWithErrors { get; set; } + + [MemoryPackOrder(19)] + public long? IncomingUnknownProtocolPackets { get; set; } + + [MemoryPackOrder(20)] + public long? OutgoingPacketsDiscarded { get; set; } + + [MemoryPackOrder(21)] + public long? OutgoingPacketsWithErrors { get; set; } + + [MemoryPackOrder(22)] + public List? Addresses { get; set; } + + [MemoryPackOrder(23)] + public List? Gateways { get; set; } + + [MemoryPackOrder(24)] + public List? Dns { get; set; } + + [MemoryPackOrder(25)] + public List? Dhcp { get; set; } + + [MemoryPackOrder(26)] + public List? Routes { get; set; } + } + + [MemoryPackable(GenerateType.Collection)] + public partial class InterfaceList : List, IAgentMessage { } + + [MemoryPackable] + public partial class Unicast : IAgentMessage + { + [MemoryPackOrder(0)] + public IPAddress2? IpAddress { get; set; } + + [MemoryPackOrder(1)] + public IPAddress2? Ipv4Mask { get; set; } + + [MemoryPackOrder(2)] + public long? AddressPreferredLifetime { get; set; } + + [MemoryPackOrder(3)] + public long? AddressValidLifetime { get; set; } + + [MemoryPackOrder(4)] + public long? DhcpLeaseLifetime { get; set; } + + [MemoryPackOrder(5)] + public DuplicateAddressDetectionState? DuplicateAddressDetectionState { get; set; } + + [MemoryPackOrder(6)] + public int? PrefixLength { get; set; } + + [MemoryPackOrder(7)] + public PrefixOrigin? PrefixOrigin { get; set; } + + [MemoryPackOrder(8)] + public SuffixOrigin? SuffixOrigin { get; set; } + } + + [MemoryPackable] + public partial class Route : IAgentMessage + { + [MemoryPackOrder(0)] + public uint? InterfaceIndex { get; set; } + + [MemoryPackOrder(1)] + public IPAddress2? Destination { get; set; } + + [MemoryPackOrder(2)] + public IPAddress2? Gateway { get; set; } + + [MemoryPackOrder(3)] + public string? Mask { get; set; } + + [MemoryPackOrder(4)] + public int? Metric { get; set; } + } + + [MemoryPackable] + public partial class IPAddress2 : IAgentMessage + { + [MemoryPackOrder(0)] + public AddressFamily? AddressFamily { get; set; } + + [MemoryPackOrder(1)] + public string? Address { get; set; } + + [MemoryPackOrder(2)] + public bool? IsIPv6Teredo { get; set; } + + [MemoryPackOrder(3)] + public bool? IsIPv6SiteLocal { get; set; } + + [MemoryPackOrder(4)] + public bool? IsIPv6Multicast { get; set; } + + [MemoryPackOrder(5)] + public bool? IsIPv6LinkLocal { get; set; } + + [MemoryPackOrder(6)] + public bool? IsIPv4MappedToIPv6 { get; set; } + + [MemoryPackOrder(7)] + public bool? IsIPv6UniqueLocal { get; set; } + + [MemoryPackConstructor] + public IPAddress2() + { + + } + + public IPAddress2(IPAddress address) + { + AddressFamily = address.AddressFamily; + Address = address.ToString(); + IsIPv4MappedToIPv6 = address.IsIPv4MappedToIPv6; + IsIPv6LinkLocal = address.IsIPv6LinkLocal; + IsIPv6Multicast = address.IsIPv6Multicast; + IsIPv6SiteLocal = address.IsIPv6SiteLocal; + IsIPv6Teredo = address.IsIPv6Teredo; + IsIPv6UniqueLocal = address.IsIPv6UniqueLocal; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Keepalive/Keepalive.cs b/src/Agent/Insight.Agent.Assets/Messages/Keepalive/Keepalive.cs new file mode 100644 index 0000000..63588dc --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Keepalive/Keepalive.cs @@ -0,0 +1,10 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(0, typeof(Keepalive))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Keepalive : IAgentMessage { } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Mainboard/Mainboard.cs b/src/Agent/Insight.Agent.Assets/Messages/Mainboard/Mainboard.cs new file mode 100644 index 0000000..a86bc5b --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Mainboard/Mainboard.cs @@ -0,0 +1,29 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(20, typeof(Mainboard))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Mainboard : IAgentMessage + { + [MemoryPackOrder(0)] + public string? Manufacturer { get; set; } + + [MemoryPackOrder(1)] + public string? Model { get; set; } + + [MemoryPackOrder(2)] + public string? Serial { get; set; } + + [MemoryPackOrder(3)] + public string? BiosManufacturer { get; set; } + + [MemoryPackOrder(4)] + public string? BiosVersion { get; set; } + + [MemoryPackOrder(5)] + public DateTime? BiosDate { get; set; } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Memory/Memory.cs b/src/Agent/Insight.Agent.Assets/Messages/Memory/Memory.cs new file mode 100644 index 0000000..bb8e444 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Memory/Memory.cs @@ -0,0 +1,67 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(40, typeof(Memory))] + [MemoryPackUnion(41, typeof(MemoryList))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Memory : IAgentMessage + { + [MemoryPackOrder(0)] + public uint? Index { get; set; } + + [MemoryPackOrder(1)] + public string? Tag { get; set; } + + [MemoryPackOrder(2)] + public string? Location { get; set; } + + [MemoryPackOrder(3)] + public string? Manufacturer { get; set; } + + [MemoryPackOrder(4)] + public string? Model { get; set; } + + [MemoryPackOrder(5)] + public string? Serial { get; set; } + + [MemoryPackOrder(6)] + public ulong? Capacity { get; set; } + + [MemoryPackOrder(7)] + public uint? Speed { get; set; } + + [MemoryPackOrder(8)] + public uint? Voltage { get; set; } + + [MemoryPackOrder(9)] + public uint? ConfiguredSpeed { get; set; } + + [MemoryPackOrder(10)] + public uint? ConfiguredVoltage { get; set; } + } + + [MemoryPackable(GenerateType.Collection)] + public partial class MemoryList : List, IAgentMessage { } + + [MemoryPackable] + public partial class MemoryMetric : IAgentMessage + { + [MemoryPackOrder(0)] + public DateTime? Timestamp { get; set; } + + [MemoryPackOrder(1)] + public float? MemoryAvailable { get; set; } + + [MemoryPackOrder(2)] + public float? MemoryAvailablePercentage { get; set; } + + [MemoryPackOrder(3)] + public float? MemoryUsed { get; set; } + + [MemoryPackOrder(4)] + public float? MemoryUsagePercentage { get; set; } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/OperationSystem/OperationSystem.cs b/src/Agent/Insight.Agent.Assets/Messages/OperationSystem/OperationSystem.cs new file mode 100644 index 0000000..0fe4ee5 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/OperationSystem/OperationSystem.cs @@ -0,0 +1,30 @@ +using MemoryPack; +using System.Runtime.InteropServices; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(90, typeof(OperationSystem))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class OperationSystem : IAgentMessage + { + [MemoryPackOrder(0)] + public string? Name { get; set; } + + [MemoryPackOrder(1)] + public string? Version { get; set; } + + [MemoryPackOrder(2)] + public string? SerialNumber { get; set; } + + [MemoryPackOrder(3)] + public Architecture? Architecture { get; set; } + + [MemoryPackOrder(4)] + public bool? Virtual { get; set; } + + [MemoryPackOrder(5)] + public DateTime? InstallDate { get; set; } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Printer/Printer.cs b/src/Agent/Insight.Agent.Assets/Messages/Printer/Printer.cs new file mode 100644 index 0000000..0478fc0 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Printer/Printer.cs @@ -0,0 +1,30 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(140, typeof(Printer))] + [MemoryPackUnion(141, typeof(PrinterList))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Printer : IAgentMessage + { + [MemoryPackOrder(0)] + public string? Name { get; set; } + + [MemoryPackOrder(1)] + public string? Driver { get; set; } + + [MemoryPackOrder(2)] + public string? Port { get; set; } + + [MemoryPackOrder(3)] + public string? Location { get; set; } + + [MemoryPackOrder(4)] + public string? Comment { get; set; } + } + + [MemoryPackable(GenerateType.Collection)] + public partial class PrinterList : List, IAgentMessage { } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Processor/Processor.cs b/src/Agent/Insight.Agent.Assets/Messages/Processor/Processor.cs new file mode 100644 index 0000000..edd63af --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Processor/Processor.cs @@ -0,0 +1,70 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(30, typeof(Processor))] + [MemoryPackUnion(31, typeof(ProcessorList))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Processor : IAgentMessage + { + [MemoryPackOrder(0)] + public uint? Index { get; set; } + + [MemoryPackOrder(1)] + public string? Name { get; set; } + + [MemoryPackOrder(2)] + public string? Manufacturer { get; set; } + + [MemoryPackOrder(3)] + public string? SerialNumber { get; set; } + + [MemoryPackOrder(4)] + public string? Socket { get; set; } + + [MemoryPackOrder(5)] + public string? Version { get; set; } + + [MemoryPackOrder(6)] + public string? DeviceId { get; set; } + + [MemoryPackOrder(7)] + public uint? Cores { get; set; } + + [MemoryPackOrder(8)] + public uint? LogicalCores { get; set; } + + [MemoryPackOrder(9)] + public uint? CurrentSpeed { get; set; } + + [MemoryPackOrder(10)] + public uint? MaxSpeed { get; set; } + + [MemoryPackOrder(11)] + public uint? L1Size { get; set; } + + [MemoryPackOrder(12)] + public uint? L2Size { get; set; } + + [MemoryPackOrder(13)] + public uint? L3Size { get; set; } + + [MemoryPackOrder(14)] + public bool? Virtualization { get; set; } + } + + [MemoryPackable(GenerateType.Collection)] + public partial class ProcessorList : List, IAgentMessage { } + + [MemoryPackable] + public partial class ProcessorMetric : IAgentMessage + { + [MemoryPackOrder(0)] + public DateTime? Timestamp { get; set; } + + [MemoryPackOrder(1)] + public float? ProcessorUsagePercentage { get; set; } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Service/Service.cs b/src/Agent/Insight.Agent.Assets/Messages/Service/Service.cs new file mode 100644 index 0000000..61aa86d --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Service/Service.cs @@ -0,0 +1,64 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(130, typeof(Service))] + [MemoryPackUnion(131, typeof(ServiceList))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Service : IAgentMessage + { + [MemoryPackOrder(0)] + public uint? ProcessId { get; set; } + + [MemoryPackOrder(1)] + public string? Name { get; set; } + + [MemoryPackOrder(2)] + public string? Display { get; set; } + + [MemoryPackOrder(3)] + public string? Description { get; set; } + + [MemoryPackOrder(4)] + public string? PathName { get; set; } + + [MemoryPackOrder(5)] + public string? Account { get; set; } + + [MemoryPackOrder(6)] + public bool? Delay { get; set; } + + [MemoryPackOrder(7)] + public ServiceStatus? Status { get; set; } + + [MemoryPackOrder(8)] + public ServiceMode? StartMode { get; set; } + + public enum ServiceStatus + { + Unknown = -1, + Stopped = 1, + StartPending = 2, + StopPending = 3, + Running = 4, + ContinuePending = 5, + PausePending = 6, + Paused = 7 + } + + public enum ServiceMode + { + Unknown = -1, + Boot = 0, + System = 1, + Automatic = 2, + Manual = 3, + Disabled = 4 + } + } + + [MemoryPackable(GenerateType.Collection)] + public partial class ServiceList : List, IAgentMessage { } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Session/Session.cs b/src/Agent/Insight.Agent.Assets/Messages/Session/Session.cs new file mode 100644 index 0000000..625c4ef --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Session/Session.cs @@ -0,0 +1,30 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(110, typeof(Session))] + [MemoryPackUnion(111, typeof(SessionList))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Session : IAgentMessage + { + [MemoryPackOrder(0)] + public string? Sid { get; set; } + + [MemoryPackOrder(1)] + public string? User { get; set; } + + [MemoryPackOrder(2)] + public string? Type { get; set; } + + [MemoryPackOrder(3)] + public string? Status { get; set; } + + [MemoryPackOrder(4)] + public string? Remote { get; set; } + } + + [MemoryPackable(GenerateType.Collection)] + public partial class SessionList : List, IAgentMessage { } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Status/Status.cs b/src/Agent/Insight.Agent.Assets/Messages/Status/Status.cs new file mode 100644 index 0000000..de31591 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Status/Status.cs @@ -0,0 +1,17 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(3, typeof(Status))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Status : IAgentMessage + { + [MemoryPackOrder(0)] + public DateTime Timestamp { get; } = DateTime.Now; + + [MemoryPackOrder(1)] + public TimeSpan Uptime { get; set; } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/StoragePool/StoragePool.cs b/src/Agent/Insight.Agent.Assets/Messages/StoragePool/StoragePool.cs new file mode 100644 index 0000000..6148979 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/StoragePool/StoragePool.cs @@ -0,0 +1,302 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(160, typeof(StoragePool))] + [MemoryPackUnion(161, typeof(StoragePoolList))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class StoragePool : IAgentMessage + { + [MemoryPackOrder(0)] + public string? UniqueId { get; set; } + + [MemoryPackOrder(1)] + public string? Name { get; set; } + + [MemoryPackOrder(2)] + public string? FriendlyName { get; set; } + + [MemoryPackOrder(3)] + public List? States { get; set; } + + [MemoryPackOrder(4)] + public HealthState? Health { get; set; } + + [MemoryPackOrder(5)] + public RetireMissingPhysicalDisksEnum? RetireMissingPhysicalDisks { get; set; } + + [MemoryPackOrder(6)] + public string? Resiliency { get; set; } + + [MemoryPackOrder(7)] + public bool? IsPrimordial { get; set; } + + [MemoryPackOrder(8)] + public bool? IsReadOnly { get; set; } + + [MemoryPackOrder(9)] + public bool? IsClustered { get; set; } + + [MemoryPackOrder(10)] + public ulong? Size { get; set; } + + [MemoryPackOrder(11)] + public ulong? AllocatedSize { get; set; } + + [MemoryPackOrder(12)] + public ulong? SectorSize { get; set; } + + [MemoryPackOrder(13)] + public List? PhysicalDisks { get; set; } + + [MemoryPackOrder(14)] + public List? VirtualDisks { get; set; } + + public enum OperationalState + { + Unknown = 0, + Other = 1, + OK = 2, + Degraded = 3, + Stressed = 4, + Predictive_Failure = 5, + Error = 6, + Non_Recoverable_Error = 7, + Starting = 8, + Stopping = 9, + Stopped = 10, + In_Service = 11, + No_Contact = 12, + Lost_Communication = 13, + Aborted = 14, + Dormant = 15, + Supporting_Entity_In_Error = 16, + Completed = 17, + Power_Mode = 18, + Relocating = 19 + } + + public enum HealthState + { + Healthy = 0, + Warning = 1, + Unhealthy = 2, + Unknown = 3 + } + + public enum RetireMissingPhysicalDisksEnum + { + Auto = 1, + Always = 2, + Never = 3 + } + } + + [MemoryPackable(GenerateType.Collection)] + public partial class StoragePoolList : List, IAgentMessage { } + + [MemoryPackable] + public partial class PhysicalDisk : IAgentMessage + { + [MemoryPackOrder(0)] + public string? UniqueId { get; set; } + + [MemoryPackOrder(1)] + public string? DeviceId { get; set; } + + [MemoryPackOrder(2)] + public string? FriendlyName { get; set; } + + [MemoryPackOrder(3)] + public string? Manufacturer { get; set; } + + [MemoryPackOrder(4)] + public string? Model { get; set; } + + [MemoryPackOrder(5)] + public ushort? MediaType { get; set; } + + [MemoryPackOrder(6)] + public ushort? BusType { get; set; } + + [MemoryPackOrder(7)] + public List? States { get; set; } + + [MemoryPackOrder(8)] + public HealthState? Health { get; set; } + + [MemoryPackOrder(9)] + public List? SupportedUsages { get; set; } + + [MemoryPackOrder(10)] + public ushort? Usage { get; set; } + + [MemoryPackOrder(11)] + public string? PhysicalLocation { get; set; } + + [MemoryPackOrder(12)] + public string? SerialNumber { get; set; } + + [MemoryPackOrder(13)] + public string? FirmwareVersion { get; set; } + + [MemoryPackOrder(14)] + public ulong? Size { get; set; } + + [MemoryPackOrder(15)] + public ulong? AllocatedSize { get; set; } + + [MemoryPackOrder(16)] + public ulong? LogicalSectorSize { get; set; } + + [MemoryPackOrder(17)] + public ulong? PhysicalSectorSize { get; set; } + + [MemoryPackOrder(18)] + public ulong? VirtualDiskFootprint { get; set; } + + public enum OperationalState + { + Unknown = 0, + Other = 1, + OK = 2, + Degraded = 3, + Stressed = 4, + Predictive_Failure = 5, + Error = 6, + Non_Recoverable_Error = 7, + Starting = 8, + Stopping = 9, + Stopped = 10, + In_Service = 11, + No_Contact = 12, + Lost_Communication = 13, + Aborted = 14, + Dormant = 15, + Supporting_Entity_In_Error = 16, + Completed = 17, + Power_Mode = 18, + Relocating = 19 + } + + public enum HealthState + { + Healthy = 0, + Warning = 1, + Unhealthy = 2, + Unknown = 3 + } + + public enum SupportedUsagesEnum + { + Unknown = 0, + Auto_Select = 1, + Manual_Select = 2, + Hot_Spare = 3, + Retired = 4, + Journal = 5 + } + } + + [MemoryPackable] + public partial class VirtualDisk : IAgentMessage + { + [MemoryPackOrder(0)] + public string? UniqueId { get; set; } + + [MemoryPackOrder(1)] + public string? Name { get; set; } + + [MemoryPackOrder(2)] + public string? FriendlyName { get; set; } + + [MemoryPackOrder(3)] + public List? States { get; set; } + + [MemoryPackOrder(4)] + public HealthState? Health { get; set; } + + [MemoryPackOrder(5)] + public AccessTypeEnum? AccessType { get; set; } + + [MemoryPackOrder(6)] + public ProvisioningTypeEnum? ProvisioningType { get; set; } + + [MemoryPackOrder(7)] + public ushort? PhysicalDiskRedundancy { get; set; } + + [MemoryPackOrder(8)] + public string? ResiliencySettingName { get; set; } + + [MemoryPackOrder(9)] + public bool? Deduplication { get; set; } + + [MemoryPackOrder(10)] + public bool? IsSnapshot { get; set; } + + [MemoryPackOrder(11)] + public ulong? Size { get; set; } + + [MemoryPackOrder(12)] + public ulong? AllocatedSize { get; set; } + + [MemoryPackOrder(13)] + public ulong? FootprintOnPool { get; set; } + + [MemoryPackOrder(14)] + public ulong? ReadCacheSize { get; set; } + + [MemoryPackOrder(15)] + public ulong? WriteCacheSize { get; set; } + + public enum OperationalState + { + Unknown = 0, + Other = 1, + OK = 2, + Degraded = 3, + Stressed = 4, + Predictive_Failure = 5, + Error = 6, + Non_Recoverable_Error = 7, + Starting = 8, + Stopping = 9, + Stopped = 10, + In_Service = 11, + No_Contact = 12, + Lost_Communication = 13, + Aborted = 14, + Dormant = 15, + Supporting_Entity_In_Error = 16, + Completed = 17, + Power_Mode = 18, + Relocating = 19 + } + + public enum HealthState + { + Healthy = 0, + Warning = 1, + Unhealthy = 2, + Unknown = 3 + } + + public enum AccessTypeEnum + { + Unknown = 0, + Readable = 1, + Writeable = 2, + Read_Write = 3, + Write_Once = 4 + } + + public enum ProvisioningTypeEnum + { + Unknown = 0, + Thin = 1, + Fixed = 2 + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/SystemInfo/SystemInfo.cs b/src/Agent/Insight.Agent.Assets/Messages/SystemInfo/SystemInfo.cs new file mode 100644 index 0000000..8ec0441 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/SystemInfo/SystemInfo.cs @@ -0,0 +1,23 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(80, typeof(SystemInfo))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class SystemInfo : IAgentMessage + { + [MemoryPackOrder(0)] + public DateTime? LastBootUpTime { get; set; } + + [MemoryPackOrder(1)] + public DateTime? LocalDateTime { get; set; } + + [MemoryPackOrder(2)] + public uint? Processes { get; set; } + + [MemoryPackOrder(3)] + public string? License { get; set; } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Trap/Trap.cs b/src/Agent/Insight.Agent.Assets/Messages/Trap/Trap.cs new file mode 100644 index 0000000..d0a66db --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Trap/Trap.cs @@ -0,0 +1,29 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(11, typeof(Trap))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Trap : IAgentMessage + { + [MemoryPackOrder(0)] + public DateTime? Timestamp { get; set; } + + [MemoryPackOrder(1)] + public string? Endpoint { get; set; } + + [MemoryPackOrder(2)] + public string? Hostname { get; set; } + + [MemoryPackOrder(3)] + public string? Version { get; set; } + + [MemoryPackOrder(4)] + public string? Community { get; set; } + + [MemoryPackOrder(5)] + public List>? Data { get; set; } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Update/Update.cs b/src/Agent/Insight.Agent.Assets/Messages/Update/Update.cs new file mode 100644 index 0000000..ecc84c1 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Update/Update.cs @@ -0,0 +1,83 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(100, typeof(Update))] + [MemoryPackUnion(101, typeof(UpdateList))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Update : IAgentMessage + { + [MemoryPackOrder(0)] + public string? Id { get; set; } + + [MemoryPackOrder(1)] + public DateTime? Date { get; set; } + + [MemoryPackOrder(2)] + public string? Name { get; set; } + + [MemoryPackOrder(3)] + public string? Description { get; set; } + + [MemoryPackOrder(4)] + public string? SupportUrl { get; set; } + + [MemoryPackOrder(5)] + public string? Hotfix { get; set; } + + // if installed + [MemoryPackOrder(6)] + public OsUpdateResultCodeEnum? Result { get; set; } + + // if pending + [MemoryPackOrder(7)] + public OsUpdateTypeEnum? Type { get; set; } + + [MemoryPackOrder(8)] + public decimal? Size { get; set; } + + [MemoryPackOrder(9)] + public bool? IsDownloaded { get; set; } + + [MemoryPackOrder(10)] + public bool? CanRequestUserInput { get; set; } + + [MemoryPackOrder(11)] + public OsUpdateRebootBehaviorEnum? RebootBehavior { get; set; } + + public enum OsUpdateRebootBehaviorEnum + { + NeverReboots = 1, + AlwaysRequiresReboot = 2, + CanRequestReboot = 3 + } + + public enum OsUpdateResultCodeEnum + { + NotStarted = 1, + InProgress = 2, + Succeeded = 3, + SucceededWithErrors = 4, + Failed = 5, + Aborted = 6 + } + + public enum OsUpdateTypeEnum + { + Software = 1, + Driver = 2 + } + } + + [MemoryPackable] + public partial class UpdateList : IAgentMessage + { + [MemoryPackOrder(0)] + public List? Installed { get; set; } + + [MemoryPackOrder(1)] + public List? Pending { get; set; } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/User/User.cs b/src/Agent/Insight.Agent.Assets/Messages/User/User.cs new file mode 100644 index 0000000..968c4bb --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/User/User.cs @@ -0,0 +1,73 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(150, typeof(User))] + [MemoryPackUnion(151, typeof(UserList))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class User : IAgentMessage + { + [MemoryPackOrder(0)] + public string? Sid { get; set; } + + [MemoryPackOrder(1)] + public string? Domain { get; set; } + + [MemoryPackOrder(2)] + public string? Name { get; set; } + + [MemoryPackOrder(3)] + public string? FullName { get; set; } + + [MemoryPackOrder(4)] + public string? Description { get; set; } + + [MemoryPackOrder(5)] + public string? Status { get; set; } + + [MemoryPackOrder(6)] + public bool? LocalAccount { get; set; } + + [MemoryPackOrder(7)] + public bool? Disabled { get; set; } + + [MemoryPackOrder(8)] + public bool? Lockout { get; set; } + + [MemoryPackOrder(9)] + public bool? PasswordChangeable { get; set; } + + [MemoryPackOrder(10)] + public bool? PasswordExpires { get; set; } + + [MemoryPackOrder(11)] + public bool? PasswordRequired { get; set; } + + [MemoryPackOrder(12)] + public List? Groups { get; set; } + } + + [MemoryPackable(GenerateType.Collection)] + public partial class UserList : List, IAgentMessage { } + + [MemoryPackable] + public partial class Group : IAgentMessage + { + [MemoryPackOrder(0)] + public string? Sid { get; set; } + + [MemoryPackOrder(1)] + public string? Domain { get; set; } + + [MemoryPackOrder(2)] + public string? Name { get; set; } + + [MemoryPackOrder(3)] + public string? Description { get; set; } + + [MemoryPackOrder(4)] + public bool? LocalAccount { get; set; } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/Videocard/Videocard.cs b/src/Agent/Insight.Agent.Assets/Messages/Videocard/Videocard.cs new file mode 100644 index 0000000..151d441 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/Videocard/Videocard.cs @@ -0,0 +1,30 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(60, typeof(Videocard))] + [MemoryPackUnion(61, typeof(VideocardList))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class Videocard : IAgentMessage + { + [MemoryPackOrder(0)] + public string? DeviceId { get; set; } + + [MemoryPackOrder(1)] + public string? Model { get; set; } + + [MemoryPackOrder(2)] + public ulong Memory { get; set; } + + [MemoryPackOrder(3)] + public DateTime DriverDate { get; set; } + + [MemoryPackOrder(4)] + public string? DriverVersion { get; set; } + } + + [MemoryPackable(GenerateType.Collection)] + public partial class VideocardList : List, IAgentMessage { } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Messages/VirtualMaschine/VirtualMaschine.cs b/src/Agent/Insight.Agent.Assets/Messages/VirtualMaschine/VirtualMaschine.cs new file mode 100644 index 0000000..a91dc44 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Messages/VirtualMaschine/VirtualMaschine.cs @@ -0,0 +1,265 @@ +using MemoryPack; + +namespace Insight.Agent.Messages +{ + [MemoryPackUnion(170, typeof(VirtualMaschine))] + [MemoryPackUnion(171, typeof(VirtualMaschineList))] + public partial interface IAgentMessage { } + + [MemoryPackable] + public partial class VirtualMaschine : IAgentMessage + { + [MemoryPackOrder(0)] + public Guid? Id { get; set; } + + [MemoryPackOrder(1)] + public uint? ProcessId { get; set; } + + [MemoryPackOrder(2)] + public string? Caption { get; set; } + + [MemoryPackOrder(3)] + public string? Name { get; set; } + + [MemoryPackOrder(4)] + public string? Notes { get; set; } + + [MemoryPackOrder(5)] + public EnabledEnum? Enabled { get; set; } + + [MemoryPackOrder(6)] + public EnabledDefaultEnum? EnabledDefault { get; set; } + + [MemoryPackOrder(7)] + public HealthStatusEnum? HealthState { get; set; } + + [MemoryPackOrder(8)] + public string? Status { get; set; } + + [MemoryPackOrder(9)] + public ulong? OnTime { get; set; } + + [MemoryPackOrder(10)] + public uint? ReplicationMode { get; set; } + + [MemoryPackOrder(11)] + public ReplicationStateEnum? ReplicationState { get; set; } + + [MemoryPackOrder(12)] + public ReplicationHealthEnum? ReplicationHealth { get; set; } + + [MemoryPackOrder(13)] + public string? ConfigurationVersion { get; set; } + + [MemoryPackOrder(14)] + public IntegrationServicesVersionStateEnum? IntegrationServicesVersionState { get; set; } + + [MemoryPackOrder(15)] + public uint? NumberOfProcessors { get; set; } + + [MemoryPackOrder(16)] + public uint? ProcessorLoad { get; set; } + + [MemoryPackOrder(17)] + public int? MemoryAvailable { get; set; } + + [MemoryPackOrder(18)] + public ulong? MemoryUsage { get; set; } + + [MemoryPackOrder(19)] + public DateTime? InstallDate { get; set; } + + [MemoryPackOrder(20)] + public DateTime? TimeOfLastConfigurationChange { get; set; } + + [MemoryPackOrder(21)] + public DateTime? TimeOfLastStateChange { get; set; } + + [MemoryPackOrder(22)] + public DateTime? LastReplicationTime { get; set; } + + [MemoryPackOrder(23)] + public string? GuestOperatingSystem { get; set; } + + [MemoryPackOrder(24)] + public List? Configurations { get; set; } + + public enum EnabledEnum + { + Unbekannt = 0, + Andere = 1, + Aktiviert = 2, + Deaktiviert = 3, + Herunterfahren = 4, + Nicht_Verfügbar = 5, + Aktiviert_Offline = 6, + In_Test = 7, + Latent = 8, + Eingeschränkt = 9, + Wird_gestartet = 10 + } + + public enum EnabledDefaultEnum + { + Aktiviert = 2, + Deaktiviert = 3, + Aktiviert_Offline = 6 + } + + public enum HealthStatusEnum + { + OK = 5, + Hauptfehler = 20, + Kritischer_Fehler = 25 + } + + public enum ReplicationStateEnum + { + Deaktiviert = 0, + Bereit = 1, + Warten_auf_Erstreplikation = 2, + Replikat = 3, + Synchronisierte_Replication_abgeschlossen = 4, + Wiederhergestellt = 5, + Commit = 6, + Angehalten = 7, + Kritisch = 8, + Warten_auf_die_Neusynchronisierung = 9, + Resynchronisierung = 10, + Resynchronisierung_angehalten = 11, + Failover_in_Bearbeitung = 12, + Failback_in_Fortschritt = 13, + Failback_abgeschlossen = 14, + Datenträgerupdate_in_Bearbeitung = 15, + Datenträgeraktualisierung_kritisch = 16, + Unbekannt = 17, + Repurpose_Replikation_in_Bearbeitung = 18, + Vorbereitet_für_die_Synchronisierungsreplikation = 19, + Vorbereitet_für_die_Umgekehrte_Replikation_der_Gruppe = 20, + Failover_in_Fortschritt = 21 + } + + public enum ReplicationHealthEnum + { + OK = 1, + Warnung = 2, + Kritisch = 3 + } + + public enum IntegrationServicesVersionStateEnum + { + Unknown = 0, + UpToDate = 1, + Mismatch = 2 + } + } + + [MemoryPackable(GenerateType.Collection)] + public partial class VirtualMaschineList : List, IAgentMessage { } + + [MemoryPackable] + public partial class VirtualMaschineConfiguration : IAgentMessage + { + [MemoryPackOrder(0)] + public string? Id { get; set; } + + [MemoryPackOrder(1)] + public string? ParentId { get; set; } + + [MemoryPackOrder(2)] + public string? Type { get; set; } + + [MemoryPackOrder(3)] + public string? Name { get; set; } + + [MemoryPackOrder(4)] + public DateTime? CreationTime { get; set; } + + [MemoryPackOrder(5)] + public string? Generation { get; set; } + + [MemoryPackOrder(6)] + public string? Architecture { get; set; } + + [MemoryPackOrder(7)] + public AutomaticStartupActionEnum? AutomaticStartupAction { get; set; } + //public DateTime? AutomaticStartupActionDelay { get; set; } + + [MemoryPackOrder(8)] + public AutomaticShutdownActionEnum? AutomaticShutdownAction { get; set; } + + [MemoryPackOrder(9)] + public AutomaticRecoveryActionEnum? AutomaticRecoveryAction { get; set; } + + [MemoryPackOrder(10)] + public bool? AutomaticSnapshotsEnabled { get; set; } + + [MemoryPackOrder(11)] + public string? BaseBoardSerialNumber { get; set; } + + [MemoryPackOrder(12)] + public string? BIOSGUID { get; set; } + + [MemoryPackOrder(13)] + public string? BIOSSerialNumber { get; set; } + + [MemoryPackOrder(14)] + public ushort[]? BootOrder { get; set; } + + [MemoryPackOrder(15)] + public string? ConfigurationDataRoot { get; set; } + + [MemoryPackOrder(16)] + public string? ConfigurationFile { get; set; } + + [MemoryPackOrder(17)] + public string? GuestStateDataRoot { get; set; } + + [MemoryPackOrder(18)] + public string? GuestStateFile { get; set; } + + [MemoryPackOrder(19)] + public string? SnapshotDataRoot { get; set; } + + [MemoryPackOrder(20)] + public string? SuspendDataRoot { get; set; } + + [MemoryPackOrder(21)] + public string? SwapFileDataRoot { get; set; } + + [MemoryPackOrder(22)] + public bool? SecureBootEnabled { get; set; } + + [MemoryPackOrder(23)] + public bool? IsAutomaticSnapshot { get; set; } + + [MemoryPackOrder(24)] + public string[]? Notes { get; set; } + + [MemoryPackOrder(25)] + public List? Childs { get; set; } + + //public string[]? HostResource { get; set; } + + public enum AutomaticStartupActionEnum + { + Nothing = 2, + RestartIfLastStateActive = 3, + Alway = 4 + } + + public enum AutomaticShutdownActionEnum + { + Ausschalten = 2, + Speichern = 3, + Herunterfahren = 4 + } + + public enum AutomaticRecoveryActionEnum + { + Keine = 2, + Neustart = 3, + Rollback = 4 + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent.Assets/Models/Config.cs b/src/Agent/Insight.Agent.Assets/Models/Config.cs new file mode 100644 index 0000000..4be9391 --- /dev/null +++ b/src/Agent/Insight.Agent.Assets/Models/Config.cs @@ -0,0 +1,7 @@ +namespace Insight.Agent.Models +{ + public class Config + { + public Guid? Serial { get; set; } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Assemblies/Interop.WUApiLib.dll b/src/Agent/Insight.Agent/Assemblies/Interop.WUApiLib.dll new file mode 100644 index 0000000..990990c Binary files /dev/null and b/src/Agent/Insight.Agent/Assemblies/Interop.WUApiLib.dll differ diff --git a/src/Agent/Insight.Agent/Constants/Appsettings.cs b/src/Agent/Insight.Agent/Constants/Appsettings.cs new file mode 100644 index 0000000..9a45fe7 --- /dev/null +++ b/src/Agent/Insight.Agent/Constants/Appsettings.cs @@ -0,0 +1,10 @@ +namespace Insight.Agent +{ + public static class Appsettings + { + public const string Api = "api"; + public const string ServerHost = "server.host"; + public const string ServerPort = "server.port"; + public const string TrapPort = "trap.port"; + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Constants/Deploy.cs b/src/Agent/Insight.Agent/Constants/Deploy.cs new file mode 100644 index 0000000..35aa19b --- /dev/null +++ b/src/Agent/Insight.Agent/Constants/Deploy.cs @@ -0,0 +1,21 @@ +namespace Insight.Agent.Constants +{ + public static class Deploy + { + public static class Updater + { + public const string Name = "Updater"; + public const string ServiceName = "insight_updater"; + public const string Description = "Insight Updater"; + } + + public static DirectoryInfo GetAppDirectory(string appName) + => new($"{Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)}/Webmatic/Insight/{appName}"); + + public static FileInfo GetAppExecutable(string appName) + => new($"{GetAppDirectory(appName).FullName}/{appName.ToLower()}.exe"); + + public static Uri GetUpdateHref(Uri api, string appName) + => new($"{api.AbsoluteUri}/update/{appName.ToLower()}/windows"); + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Extensions/Configuration.cs b/src/Agent/Insight.Agent/Extensions/Configuration.cs new file mode 100644 index 0000000..c5df5da --- /dev/null +++ b/src/Agent/Insight.Agent/Extensions/Configuration.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Configuration; + +namespace Insight.Agent.Extensions +{ + public static class ConfigurationExtensions + { + public static IConfigurationBuilder Defaults(this IConfigurationBuilder configuration) + { + configuration.Sources.Clear(); + configuration.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); + return configuration.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", optional: true, reloadOnChange: true); + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Extensions/Linux.cs b/src/Agent/Insight.Agent/Extensions/Linux.cs new file mode 100644 index 0000000..da4f85a --- /dev/null +++ b/src/Agent/Insight.Agent/Extensions/Linux.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; +using System.Runtime.Versioning; + +namespace Insight.Agent.Extensions +{ + public static class Linux + { + [SupportedOSPlatform("linux")] + public static string Bash(this string cmd) + { + var escaped = cmd.Replace("\"", "\\\""); + + using var proc = new Process() + { + StartInfo = new ProcessStartInfo + { + FileName = "/bin/bash", + Arguments = $"-c \"{escaped}\"", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + + proc.Start(); + var result = proc.StandardOutput.ReadToEnd(); + proc.WaitForExit(); + + return result; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Insight.Agent.csproj b/src/Agent/Insight.Agent/Insight.Agent.csproj new file mode 100644 index 0000000..59ff570 --- /dev/null +++ b/src/Agent/Insight.Agent/Insight.Agent.csproj @@ -0,0 +1,53 @@ + + + + Exe + net7.0 + Insight.Agent + Insight + agent + 2023.9.14.0 + enable + enable + + + + none + + + + none + + + + + + + + + + + + + + + + + + + Assemblies\Interop.WUApiLib.dll + True + False + + + + + + Always + + + Always + + + + diff --git a/src/Agent/Insight.Agent/Internals/Extensions.cs b/src/Agent/Insight.Agent/Internals/Extensions.cs new file mode 100644 index 0000000..9699249 --- /dev/null +++ b/src/Agent/Insight.Agent/Internals/Extensions.cs @@ -0,0 +1,69 @@ +using System.Management; +using System.Runtime.Versioning; + +namespace Insight.Agent +{ + public static class ManagmentExtensions + { + [SupportedOSPlatform("windows")] + public static HashSet GetPropertyHashes(this ManagementBaseObject @object) + { + var properties = new HashSet(); + + foreach (var property in @object.Properties) + { + properties.Add(property.Name); + } + + return properties; + } + + [SupportedOSPlatform("windows")] + internal static bool TryGet(this ManagementObjectSearcher searcher, out ManagementObjectCollection collection) + { + collection = searcher.Get(); + + try + { + _ = collection.Count; + return true; + } + catch (ManagementException) + { + collection.Dispose(); + return false; + } + } + + [SupportedOSPlatform("windows")] + internal static T? GetValue(this ManagementObject @object, HashSet properties, string key) + { + if (@object is null || properties is null || key is null) return default; + if (properties.Contains(key, StringComparer.OrdinalIgnoreCase) is false) return default; + + if (@object[key] is not T obj) + { + return default; + } + + return obj; + } + + [SupportedOSPlatform("windows")] + internal static bool TryGetValue(this ManagementBaseObject @object, HashSet properties, string key, out T? value) + { + value = default; + + if (@object is null || properties is null || key is null) return default; + if (properties.Contains(key, StringComparer.OrdinalIgnoreCase) is false) return false; + + if (@object[key] is not T obj) + { + return false; + } + + value = obj; + return true; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Internals/Helpers.cs b/src/Agent/Insight.Agent/Internals/Helpers.cs new file mode 100644 index 0000000..93d7e62 --- /dev/null +++ b/src/Agent/Insight.Agent/Internals/Helpers.cs @@ -0,0 +1,44 @@ +using System.Runtime.Versioning; +using System.ServiceProcess; +using System.Text; + +namespace Insight.Agent +{ + internal class Helpers + { + internal static string? EscapeWql(string text) + { + if (text == null) return null; + + var sb = new StringBuilder(text.Length); + foreach (var c in text) + { + if (c == '\\' || c == '\'' || c == '"') + { + sb.Append('\\'); + } + sb.Append(c); + } + return sb.ToString(); + } + + [SupportedOSPlatform("windows")] + internal static bool ForceWinRmStart() + { + var winRm = ServiceController + .GetServices() + .First(x => x.ServiceName + .ToLower() + .Equals("winrm", StringComparison.Ordinal)); + + if (winRm.Status is not ServiceControllerStatus.Running) + { + winRm.Start(); + winRm.WaitForStatus(ServiceControllerStatus.Running); + } + + if (winRm.Status != ServiceControllerStatus.Running) return false; + return true; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/AgentSession.cs b/src/Agent/Insight.Agent/Network/AgentSession.cs new file mode 100644 index 0000000..84678ea --- /dev/null +++ b/src/Agent/Insight.Agent/Network/AgentSession.cs @@ -0,0 +1,57 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Microsoft.Extensions.Logging; +using Vaitr.Network; + +namespace Insight.Agent.Network +{ + public class AgentSession : TcpSession + { + private readonly IEnumerable> _handlers; + + public AgentSession(IEnumerable> handlers, ISerializer serializer, ILogger logger) : base(serializer, logger) + { + _handlers = handlers; + } + + protected override ValueTask OnConnectedAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Agent ({ep?}) connected", RemoteEndPoint); + return default; + } + + protected override ValueTask OnDisconnectedAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Agent ({ep?}) disconnected", RemoteEndPoint); + return default; + } + + protected override ValueTask OnSentAsync(IPacketContext context, CancellationToken cancellationToken) + { + return base.OnSentAsync(context, cancellationToken); + } + + protected override async ValueTask OnReceivedAsync(IPacketContext context, CancellationToken cancellationToken) + { + await base.OnReceivedAsync(context, cancellationToken); + + foreach (var handler in _handlers) + { + try + { + await handler.HandleAsync(this, context.Packet, cancellationToken); + } + catch (Exception ex) + { + _logger.LogWarning("Agent ({ep?}) {ex}", RemoteEndPoint, ex.ToString()); + } + } + } + + protected override ValueTask OnHeartbeatAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Agent ({ep?}) Heartbeat", RemoteEndPoint); + return default; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/AuthenticationHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/AuthenticationHandler.cs new file mode 100644 index 0000000..4247693 --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/AuthenticationHandler.cs @@ -0,0 +1,38 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Agent.Models; +using Insight.Agent.Services; +using Insight.Domain.Constants; + +namespace Insight.Agent.Network.Handlers +{ + public class AuthenticationHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is AuthenticationRequest) + { + Config? config = null; + + try + { + config = await Configurator.ReadAsync(Configuration.DefaultConfig, cancellationToken).ConfigureAwait(false); + } + catch (Exception) { } + + if (config is null) + { + config = new Config { Serial = Guid.NewGuid() }; + await Configurator.WriteAsync(config, Configuration.DefaultConfig, cancellationToken).ConfigureAwait(false); + } + + await sender.SendAsync(new Authentication + { + Serial = config.Serial ?? throw new InvalidDataException(nameof(config.Serial)), + Version = Configuration.Version, + Hostname = Configuration.Hostname + }, cancellationToken); + } + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/ConsoleHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/ConsoleHandler.cs new file mode 100644 index 0000000..267bb5d --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/ConsoleHandler.cs @@ -0,0 +1,111 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using System.Management.Automation; +using System.Management.Automation.Runspaces; + +namespace Insight.Agent.Network.Handlers; + +public class ConsoleHandler : IAgentMessageHandler +{ + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is ConsoleQueryRequest consoleQueryRequest) + { + await OnConsoleQueryRequestAsync(sender, consoleQueryRequest, cancellationToken); + } + } + + private async ValueTask OnConsoleQueryRequestAsync(AgentSession sender, ConsoleQueryRequest consoleQueryRequest, CancellationToken cancellationToken) + { + var result = await QueryScriptAsync(consoleQueryRequest.Query); + + await sender.SendAsync(new ConsoleQuery + { + Id = consoleQueryRequest.Id, + HostId = consoleQueryRequest.HostId, + Query = consoleQueryRequest.Query, + Data = result.Data, + Errors = result.Errors, + HadErrors = result.HadErrors + }, cancellationToken); + } + + private static async Task QueryScriptAsync(string query) + { + var result = new QueryResult(); + var errors = new List(); + + try + { + using var runspace = RunspaceFactory.CreateRunspace(); + runspace.Open(); + runspace.SessionStateProxy.LanguageMode = PSLanguageMode.FullLanguage; + + using var ps = PowerShell.Create(runspace); + ps.AddScript("Set-ExecutionPolicy unrestricted -Scope Process"); + ps.AddScript(query); + ps.AddCommand("ConvertTo-Json"); // -Depth 10 + + result.Query = query; + + var queryResult = await ps.InvokeAsync(); + + if (ps.HadErrors) + { + result.HadErrors = true; + errors.AddRange(ps.Streams.Error.Select(e => e.ToString())); + } + else + { + result.Data = queryResult[0].ToString(); + + //if (string.IsNullOrWhiteSpace(jsonString)) return result; + + //if (jsonString.TrimStart().StartsWith("[")) // It's an array + //{ + // result.IsArray = true; + + // var deserialized = JsonSerializer.Deserialize>>(jsonString, new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + // if (deserialized is null) return result; + + // result.Data.AddRange(deserialized); + + // //Console.WriteLine("Deserialized to List>"); + //} + //else + //{ + // if (jsonString.TrimStart().StartsWith("{") is false) // It's an object + // { + // result.IsString = true; + // result.Data.Add(new Dictionary { { query, jsonString.Trim('"') } }); + // } + // else + // { + // var deserialized = JsonSerializer.Deserialize>(jsonString, new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + // if (deserialized is null) return result; + + // result.Data.Add(deserialized); + + // //Console.WriteLine("Deserialized to Dictionary"); + // } + //} + } + } + catch (Exception ex) + { + result.HadErrors = true; + errors.Add(ex.Message); + } + + result.Errors = string.Join("\n", errors); + return result; + } +} + +public class QueryResult +{ + public bool HadErrors { get; set; } + public string? Query { get; set; } + public string? Data { get; set; } + public string? Errors { get; set; } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/DriveHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/DriveHandler.cs new file mode 100644 index 0000000..13fafe8 --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/DriveHandler.cs @@ -0,0 +1,178 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using System.Management; +using System.Runtime.Versioning; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + public class DriveHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + var result = new DriveList(); + result.AddRange(GetDrives()); + + await sender.SendAsync(result, cancellationToken); + } + } + + private static List GetDrives() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\cimv2"), + Query = new ObjectQuery("select index, name, caption, model, manufacturer, serialNumber, size, status, interfacetype, firmwarerevision, deviceid, pnpdeviceid from win32_diskdrive") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from win32_diskdrive"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var drives = new List(); + + using (collection) + { + foreach (ManagementObject @object in collection.Cast()) + { + var drive = new Drive(); + + var properties = @object.GetPropertyHashes(); + + drive.Index = @object.GetValue(properties, "index"); + drive.Id = @object.GetValue(properties, "deviceid")?.Trim(); + drive.Name = @object.GetValue(properties, "model")?.Trim(); + drive.Manufacturer = @object.GetValue(properties, "manufacturer")?.Trim(); + drive.SerialNumber = @object.GetValue(properties, "serialnumber")?.Trim(); + drive.Size = @object.GetValue(properties, "size"); + drive.Status = @object.GetValue(properties, "status")?.Trim(); + drive.InterfaceType = @object.GetValue(properties, "interfacetype")?.Trim(); + drive.FirmwareRevision = @object.GetValue(properties, "firmwarerevision")?.Trim(); + drive.PNPDeviceID = @object.GetValue(properties, "pnpdeviceid")?.Trim(); + drive.Volumes = new List(); + + var diskpartition = @object.GetRelated("win32_diskpartition"); + using (diskpartition) + { + foreach (ManagementObject dp in diskpartition.Cast()) + { + var volume = new Volume(); + var dpProperties = dp.GetPropertyHashes(); + + volume.NumberOfBlocks = dp.GetValue(dpProperties, "numberofblocks"); + volume.BootPartition = dp.GetValue(dpProperties, "bootpartition"); + volume.PrimaryPartition = dp.GetValue(dpProperties, "primarypartition"); + volume.Size = dp.GetValue(dpProperties, "size"); + volume.Index = dp.GetValue(dpProperties, "index"); + volume.Type = dp.GetValue(dpProperties, "type")?.Trim(); + volume.Bootable = dp.GetValue(dpProperties, "bootable"); + volume.BlockSize = dp.GetValue(dpProperties, "blocksize"); + volume.StartingOffset = dp.GetValue(dpProperties, "startingoffset"); + + var logicaldisk = dp.GetRelated("win32_logicaldisk"); + using (logicaldisk) + { + foreach (ManagementObject ld in logicaldisk.Cast()) + { + var ldProperties = ld.GetPropertyHashes(); + + volume.Id = ld.GetValue(ldProperties, "deviceid")?.Trim(); + volume.Name = ld.GetValue(ldProperties, "volumename")?.Trim(); + volume.SerialNumber = ld.GetValue(ldProperties, "volumeserialnumber")?.Trim(); + volume.DriveType = (DriveType)ld.GetValue(ldProperties, "drivetype"); + volume.FileSystem = ld.GetValue(ldProperties, "filesystem")?.Trim(); + volume.Compressed = ld.GetValue(ldProperties, "compressed"); + volume.Size = ld.GetValue(ldProperties, "size"); + volume.FreeSpace = ld.GetValue(ldProperties, "freespace"); + volume.ProviderName = ld.GetValue(ldProperties, "providername")?.Trim(); + } + } + + drive.Volumes.Add(volume); + } + } + + drives.Add(drive); + } + } + + return drives; + } + + private static List GetVolumes() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\cimv2"), + Query = new ObjectQuery("select deviceid, volumename, volumeserialnumber, drivetype, filesystem, compressed, size, freeSpace, providername from win32_logicaldisk") + }; + + // per device query + // "ASSOCIATORS OF {Win32_DiskDrive.DeviceID='" + driveDeviceId + "'} WHERE AssocClass=Win32_DiskDriveToDiskPartition" + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from win32_logicaldisk"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var volumes = new List(); + + using (collection) + { + foreach (ManagementObject @object in collection.Cast()) + { + var volume = new Volume(); + + var properties = @object.GetPropertyHashes(); + + //volume.DeviceId = @object.GetValue(properties, "deviceid")?.Trim(); + //volume.VolumeName = @object.GetValue(properties, "volumename")?.Trim(); + //volume.VolumeSerialNumber = @object.GetValue(properties, "volumeserialnumber")?.Trim(); + volume.DriveType = (DriveType)@object.GetValue(properties, "drivetype"); + volume.FileSystem = @object.GetValue(properties, "filesystem")?.Trim(); + volume.Compressed = @object.GetValue(properties, "compressed"); + volume.Size = @object.GetValue(properties, "size"); + volume.FreeSpace = @object.GetValue(properties, "freespace"); + volume.ProviderName = @object.GetValue(properties, "providername")?.Trim(); + + if (volume.Id is not null) + { + searcher.Query = new ObjectQuery("associators of {win32_logicaldisk.deviceid='" + volume.Id + "'} where assocclass=win32_logicaldisktopartition"); + + if (searcher.TryGet(out var collection2)) + { + using (collection2) + { + foreach (ManagementObject @object2 in collection2) + { + var properties2 = @object2.GetPropertyHashes(); + + volume.Index = @object2.GetValue(properties2, "index"); + //volume.DiskIndex = @object2.GetValue(properties2, "diskindex"); + volume.Type = @object2.GetValue(properties2, "type")?.Trim(); + volume.Bootable = @object2.GetValue(properties2, "bootable"); + volume.PrimaryPartition = @object2.GetValue(properties2, "primarypartition"); + volume.BootPartition = @object2.GetValue(properties2, "bootpartition"); + volume.BlockSize = @object2.GetValue(properties2, "blocksize"); + volume.NumberOfBlocks = @object2.GetValue(properties2, "numberofblocks"); + volume.StartingOffset = @object2.GetValue(properties2, "startingoffset"); + } + } + } + } + + volumes.Add(volume); + } + } + + return volumes; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/InterfaceHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/InterfaceHandler.cs new file mode 100644 index 0000000..2d6e294 --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/InterfaceHandler.cs @@ -0,0 +1,274 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using System.Management; +using System.Net; +using System.Net.NetworkInformation; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using Route = Insight.Agent.Messages.Route; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + public class InterfaceHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + var result = new InterfaceList(); + result.AddRange(GetInterfaces()); + + await sender.SendAsync(result, cancellationToken); + } + } + + private static List GetInterfaces() + { + if (NetworkInterface.GetIsNetworkAvailable() is false) return null; + if (NetworkInterface.GetAllNetworkInterfaces().Any() is false) return null; + + var interfaces = new List(); + + foreach (var ni in NetworkInterface.GetAllNetworkInterfaces()) + { + var ipProperties = ni.GetIPProperties(); + var ipStatistics = ni.GetIPStatistics(); + + var @interface = new Interface + { + Mac = ni.GetPhysicalAddress().ToString(), + Name = ni.Name, + Description = ni.Description, + Type = ni.NetworkInterfaceType, + Speed = ni.Speed, + Status = ni.OperationalStatus, + Suffix = ipProperties.DnsSuffix, + Sent = ipStatistics.BytesSent, + Received = ipStatistics.BytesReceived, + IncomingPacketsDiscarded = ipStatistics.IncomingPacketsDiscarded, + IncomingPacketsWithErrors = ipStatistics.IncomingPacketsWithErrors, + IncomingUnknownProtocolPackets = ipStatistics.IncomingUnknownProtocolPackets, + OutgoingPacketsDiscarded = ipStatistics.OutgoingPacketsDiscarded, + OutgoingPacketsWithErrors = ipStatistics.OutgoingPacketsWithErrors + }; + + try + { + var propertiesV4 = ipProperties.GetIPv4Properties(); + @interface.Index = uint.Parse(propertiesV4.Index.ToString()); + @interface.Ipv4Mtu = propertiesV4.Mtu; + @interface.Ipv4Dhcp = propertiesV4.IsDhcpEnabled; + @interface.Ipv4Forwarding = propertiesV4.IsForwardingEnabled; + } + catch (Exception) { } + + try + { + var propertiesV6 = ipProperties.GetIPv6Properties(); + @interface.Index = uint.Parse(propertiesV6.Index.ToString()); + @interface.Ipv6Mtu = propertiesV6.Mtu; + } + catch (Exception) { } + + @interface.Gateways = GetAddresses(ipProperties.GatewayAddresses); + @interface.Addresses = GetAddresses(ipProperties.UnicastAddresses); + @interface.Dns = GetAddresses(ipProperties.DnsAddresses); + @interface.Dhcp = GetAddresses(ipProperties.DhcpServerAddresses); + + if (@interface.Index.HasValue) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\cimv2"), + Query = new ObjectQuery($"select interfaceindex, guid, physicaladapter, manufacturer from win32_networkadapter where interfaceindex = {@interface.Index}") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery($"select * from win32_networkadapter where interfaceindex = {@interface.Index}"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + using (collection) + { + foreach (ManagementObject @object in collection) + { + var properties = @object.GetPropertyHashes(); + + @interface.Manufacturer = @object.GetValue(properties, "manufacturer")?.Trim(); + @interface.Guid = @object.GetValue(properties, "guid"); + @interface.Physical = @object.GetValue(properties, "physicaladapter"); + + break; + } + } + + @interface.Routes = QueryInterfaceRoutes(@interface.Index.Value); + } + } + + interfaces.Add(@interface); + } + + return interfaces; + } + + private static List QueryInterfaceRoutes(uint interfaceIndex) + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\standardcimv2"), + Query = new ObjectQuery($"select addressFamily, state, interfaceindex, routemetric, nexthop, destinationprefix from msft_netroute where interfaceindex = {interfaceIndex}") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery($"select * from msft_netroute where interfaceindex = {interfaceIndex}"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var routes = new List(); + + using (collection) + { + foreach (var @object in collection) + { + var route = new Route + { + InterfaceIndex = interfaceIndex + }; + + var properties = @object.GetPropertyHashes(); + + if (@object.TryGetValue(properties, "routemetric", out var routemetric)) + { + if (int.TryParse(routemetric?.ToString(), out var metric)) route.Metric = metric; + } + + if (@object.TryGetValue(properties, "nexthop", out var nexthop)) + { + if (IPAddress.TryParse(nexthop?.ToString(), out var gateway)) route.Gateway = new IPAddress2(gateway); + } + + if (@object.TryGetValue(properties, "destinationprefix", out var destinationprefix)) + { + var split = destinationprefix?.ToString()?.Split('/'); + var cidrData = split?[1]; + + if (IPAddress.TryParse(split?[0], out var destination)) + route.Destination = new IPAddress2(destination); + + if (int.TryParse(cidrData, out var cidr)) + { + var mask = ConvertCidr(cidr); + route.Mask = mask; + } + } + + routes.Add(route); + } + } + + return routes; + } + + private static List GetAddresses(UnicastIPAddressInformationCollection unicastCollection) + { + var addresses = new List(); + + if (unicastCollection.Any() is false) return addresses; + + foreach (var unicast in unicastCollection) + { + addresses.Add(new Unicast + { + IpAddress = new IPAddress2(unicast.Address), + AddressPreferredLifetime = unicast.AddressPreferredLifetime, + AddressValidLifetime = unicast.AddressValidLifetime, + DuplicateAddressDetectionState = unicast.DuplicateAddressDetectionState, + Ipv4Mask = new IPAddress2(unicast.IPv4Mask), + PrefixLength = unicast.PrefixLength, + PrefixOrigin = unicast.PrefixOrigin, + SuffixOrigin = unicast.SuffixOrigin, + DhcpLeaseLifetime = unicast.DhcpLeaseLifetime, + }); + } + + return addresses; + } + + private static List GetAddresses(IPAddressCollection addressCollection) + { + var addresses = new List(); + + if (addressCollection.Any() is false) return addresses; + + foreach (var address in addressCollection) + { + addresses.Add(new IPAddress2(address)); + } + + return addresses; + } + + private static List GetAddresses(GatewayIPAddressInformationCollection addressCollection) + { + var addresses = new List(); + + if (addressCollection.Any() is false) return addresses; + + foreach (var address in addressCollection) + { + addresses.Add(new IPAddress2(address.Address)); + } + + return addresses; + } + + private static string? ConvertCidr(int cidr) + { + return cidr switch + { + 0 => "0.0.0.0", + 1 => "128.0.0.0", + 2 => "192.0.0.0", + 3 => "224.0.0.0", + 4 => "240.0.0.0", + 5 => "248.0.0.0", + 6 => "252.0.0.0", + 7 => "254.0.0.0", + 8 => "255.0.0.0", + 9 => "255.128.0.0", + 10 => "255.192.0.0", + 11 => "255.224.0.0", + 12 => "255.240.0.0", + 13 => "255.248.0.0", + 14 => "255.252.0.0", + 15 => "255.254.0.0", + 16 => "255.255.0.0", + 17 => "255.255.128.0", + 18 => "255.255.192.0", + 19 => "255.255.224.0", + 20 => "255.255.240.0", + 21 => "255.255.248.0", + 22 => "255.255.252.0", + 23 => "255.255.254.0", + 24 => "255.255.255.0", + 25 => "255.255.255.128", + 26 => "255.255.255.192", + 27 => "255.255.255.224", + 28 => "255.255.255.240", + 29 => "255.255.255.248", + 30 => "255.255.255.252", + 31 => "255.255.255.254", + 32 => "255.255.255.255", + _ => null, + }; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/MainboardHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/MainboardHandler.cs new file mode 100644 index 0000000..43f210b --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/MainboardHandler.cs @@ -0,0 +1,86 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using System.Management; +using System.Runtime.Versioning; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + public class MainboardHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + await sender.SendAsync(GetMainboard(), cancellationToken); + } + } + + private static Mainboard GetMainboard() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\cimv2"), + Query = new ObjectQuery("select manufacturer, product, serialnumber from win32_baseboard") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from win32_baseboard"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var mainboard = new Mainboard(); + + using (collection) + { + foreach (ManagementObject @object in collection.Cast()) + { + var properties = @object.GetPropertyHashes(); + + mainboard.Manufacturer = @object.GetValue(properties, "manufacturer")?.Trim(); + mainboard.Model = @object.GetValue(properties, "product")?.Trim(); + mainboard.Serial = @object.GetValue(properties, "serialnumber")?.Trim(); + + break; + } + } + + searcher.Query = new ObjectQuery("select manufacturer, serialnumber, smbiosbiosversion, releasedate from win32_bios"); + + if (searcher.TryGet(out var collection2) is false) + { + searcher.Query = new ObjectQuery("select * from win32_bios"); + + if (searcher.TryGet(out collection2) is false) return null; + } + + using (collection2) + { + foreach (ManagementObject @object in collection2.Cast()) + { + var properties = @object.GetPropertyHashes(); + + mainboard.BiosManufacturer = @object.GetValue(properties, "manufacturer")?.Trim(); + mainboard.Serial = @object.GetValue(properties, "serialnumber")?.Trim(); + mainboard.BiosVersion = @object.GetValue(properties, "smbiosbiosversion")?.Trim(); + + if (@object.TryGetValue(properties, "releasedate", out var releasedate)) + { + mainboard.BiosDate = ManagementDateTimeConverter.ToDateTime(releasedate?.ToString()); + } + + break; + } + } + + //Logger.LogWarning(JsonSerializer.Serialize(mainboard, new JsonSerializerOptions + //{ + // WriteIndented= true + //})); + + return mainboard; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/MemoryHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/MemoryHandler.cs new file mode 100644 index 0000000..11ef77f --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/MemoryHandler.cs @@ -0,0 +1,123 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using System.Management; +using System.Runtime.Versioning; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + public class MemoryHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + var result = new MemoryList(); + result.AddRange(GetMemory()); + + await sender.SendAsync(result, cancellationToken); + } + } + + private static List GetMemory() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\cimv2"), + Query = new ObjectQuery("select tag, devicelocator, manufacturer, partnumber, serialnumber, capacity, speed, maxvoltage, configuredclockspeed, configuredvoltage from win32_physicalmemory") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from win32_physicalmemory"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var memorysticks = new List(); + + using (collection) + { + uint index = 0; + + foreach (ManagementObject @object in collection.Cast()) + { + var @memory = new Memory(); + + var properties = @object.GetPropertyHashes(); + + @memory.Index = index; + @memory.Tag = @object.GetValue(properties, "tag")?.Trim(); + @memory.Location = @object.GetValue(properties, "devicelocator")?.Trim(); + @memory.Manufacturer = @object.GetValue(properties, "manufacturer")?.Trim(); + @memory.Model = @object.GetValue(properties, "partnumber")?.Trim(); + @memory.Serial = @object.GetValue(properties, "serialnumber")?.Trim(); + @memory.Capacity = @object.GetValue(properties, "capacity"); + @memory.Speed = @object.GetValue(properties, "speed"); + @memory.Voltage = @object.GetValue(properties, "maxvoltage"); + @memory.ConfiguredSpeed = @object.GetValue(properties, "configuredclockspeed"); + @memory.ConfiguredVoltage = @object.GetValue(properties, "configuredvoltage"); + + memorysticks.Add(@memory); + index++; + } + } + + return memorysticks; + } + + //private async ValueTask GetMemoryMetricAsync(CancellationToken cancellationToken) + //{ + // var metric = new Memory.Metric(); + + // using var searcher = new ManagementObjectSearcher + // { + // Scope = new ManagementScope(@"root\cimv2"), + // Query = new ObjectQuery("select totalphysicalmemory from win32_computersystem") + // }; + + // if (searcher.TryGet(out var collection) is false) + // { + // searcher.Query = new ObjectQuery("select * from win32_computersystem"); + + // if (searcher.TryGet(out collection) is false) return metric; + // } + + // ulong capacity = 0; + + // using (collection) + // { + // foreach (var @object in collection) + // { + // var properties = @object.GetPropertyHashes(); + + // if (@object.TryGetValue(properties, "totalphysicalmemory", out capacity)) + // { + // capacity = capacity / 1024 / 1024; + // } + + // break; + // } + // } + + // if (MemoryAvailableCounter is null) + // { + // MemoryAvailableCounter = new PerformanceCounter + // { + // CategoryName = "Memory", + // CounterName = "Available MBytes" + // }; + + // metric.MemoryAvailable = MemoryAvailableCounter.NextValue(); + // await Task.Delay(1000, cancellationToken).ConfigureAwait(false); + // } + + // metric.Timestamp = DateTime.Now; + // metric.MemoryAvailable = MemoryAvailableCounter.NextValue(); + // metric.MemoryUsed = capacity - metric.MemoryAvailable; + // metric.MemoryUsagePercentage = metric.MemoryUsed / capacity * 100; + // metric.MemoryAvailablePercentage = 100 - metric.MemoryUsagePercentage; + // return metric; + //} + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/OperationSystemHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/OperationSystemHandler.cs new file mode 100644 index 0000000..0a36aad --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/OperationSystemHandler.cs @@ -0,0 +1,91 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Microsoft.Win32; +using System.Management; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Security.AccessControl; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + public class OperationSystemHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + await sender.SendAsync(GetOperatingSystem(), cancellationToken); + } + } + + private static OperationSystem GetOperatingSystem() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\cimv2"), + Query = new ObjectQuery("select caption, version, serialnumber, osarchitecture, installdate from win32_operatingsystem") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from win32_operatingsystem"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var os = new OperationSystem(); + + using (collection) + { + foreach (ManagementObject @object in collection) + { + var properties = @object.GetPropertyHashes(); + + os.Name = @object.GetValue(properties, "caption")?.Trim(); + os.Version = @object.GetValue(properties, "version")?.Trim(); + os.SerialNumber = @object.GetValue(properties, "serialnumber")?.Trim(); + + if (@object.TryGetValue(properties, "osarchitecture", out var architecture)) + { + if (architecture is not null && architecture.ToLower().Contains("64")) os.Architecture = Architecture.X64; + } + else + { + os.Architecture = Architecture.X86; + } + + if (@object.TryGetValue(properties, "installdate", out var installdate)) + { + os.InstallDate = ManagementDateTimeConverter.ToDateTime(installdate?.ToString()); + } + + break; + } + } + + using var registry = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Default); + using var key = registry.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion", RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey); + + if (key is not null && key?.GetValue("UBR")?.ToString() is string buildNumber) + { + os.Version = $"{os.Version}.{buildNumber}"; + } + + searcher.Query = new ObjectQuery("select * from win32_portconnector"); + + if (searcher.TryGet(out var collection2) is false) + { + os.Virtual = true; + } + else + { + os.Virtual = false; + } + + collection2.Dispose(); + + return os; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/PrinterHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/PrinterHandler.cs new file mode 100644 index 0000000..f52c60d --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/PrinterHandler.cs @@ -0,0 +1,60 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using System.Management; +using System.Runtime.Versioning; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + public class PrinterHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + var result = new PrinterList(); + result.AddRange(GetPrinters()); + + await sender.SendAsync(result, cancellationToken); + } + } + + private static List GetPrinters() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\cimv2"), + Query = new ObjectQuery("select drivername, name, portname, location, comment from win32_printer") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from win32_printer"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var printers = new List(); + + using (collection) + { + foreach (ManagementObject @object in collection.Cast()) + { + var printer = new Printer(); + + var properties = @object.GetPropertyHashes(); + + printer.Driver = @object.GetValue(properties, "drivername")?.Trim(); + printer.Name = @object.GetValue(properties, "name")?.Trim(); + printer.Port = @object.GetValue(properties, "portname")?.Trim(); + printer.Location = @object.GetValue(properties, "location")?.Trim(); + printer.Comment = @object.GetValue(properties, "comment")?.Trim(); + + printers.Add(printer); + } + } + + return printers; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/ProcessorHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/ProcessorHandler.cs new file mode 100644 index 0000000..47052cc --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/ProcessorHandler.cs @@ -0,0 +1,143 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using System.Management; +using System.Runtime.Versioning; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + public class ProcessorHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + var result = new ProcessorList(); + result.AddRange(GetProcessors()); + + await sender.SendAsync(result, cancellationToken); + } + } + + private static List GetProcessors() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\cimv2"), + Query = new ObjectQuery("select deviceid, name, manufacturer, socketdesignation, version, processorid, l2cachesize, l3cachesize, currentclockspeed, maxclockspeed, numberofcores, numberoflogicalprocessors, virtualizationfirmwareenabled from win32_processor") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from win32_processor"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var processors = new List(); + + using (collection) + { + uint index = 0; + + foreach (ManagementObject @object in collection.Cast()) + { + var processor = new Processor(); + + var properties = @object.GetPropertyHashes(); + + processor.Index = index; + processor.DeviceId = @object.GetValue(properties, "deviceid")?.Trim(); + processor.Name = @object.GetValue(properties, "name")?.Trim(); + processor.Manufacturer = @object.GetValue(properties, "manufacturer")?.Trim(); + processor.Socket = @object.GetValue(properties, "socketdesignation")?.Trim(); + processor.Version = @object.GetValue(properties, "version")?.Trim(); + processor.SerialNumber = @object.GetValue(properties, "processorid")?.Trim(); + processor.CurrentSpeed = @object.GetValue(properties, "currentclockspeed"); + processor.MaxSpeed = @object.GetValue(properties, "maxclockspeed"); + processor.Cores = @object.GetValue(properties, "numberofcores"); + processor.LogicalCores = @object.GetValue(properties, "numberoflogicalprocessors"); + processor.Virtualization = @object.GetValue(properties, "virtualizationfirmwareenabled"); + + searcher.Query = new ObjectQuery("select level, maxcachesize from win32_cachememory"); + + if (searcher.TryGet(out var collection2) is false) + { + searcher.Query = new ObjectQuery("select * from win32_cachememory"); + + if (searcher.TryGet(out collection2) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + using (collection2) + { + foreach (ManagementObject @object2 in collection2.Cast()) + { + var properties2 = @object2.GetPropertyHashes(); + + ProcessorCacheLevelEnum? cacheLevel = null; + + cacheLevel = (ProcessorCacheLevelEnum?)@object2.GetValue(properties2, "level"); + + if (cacheLevel is null) continue; + + var installedSize = @object2.GetValue(properties2, "maxcachesize"); + + switch (cacheLevel) + { + case ProcessorCacheLevelEnum.L1: + { + processor.L1Size = installedSize; + break; + } + case ProcessorCacheLevelEnum.L2: + { + processor.L2Size = installedSize; + break; + } + case ProcessorCacheLevelEnum.L3: + { + processor.L3Size = installedSize; + break; + } + } + } + } + + processors.Add(processor); + index++; + } + } + + return processors; + } + + private enum ProcessorCacheLevelEnum + { + L1 = 3, + L2 = 4, + L3 = 5, + } + + //private async ValueTask GetProcessorMetricAsync(CancellationToken cancellationToken) + //{ + // var metric = new Processor.Metric(); + + // if (ProcessorTimeCounter is null) + // { + // ProcessorTimeCounter = new PerformanceCounter + // { + // CategoryName = "Processor", + // CounterName = "% Processor Time", + // InstanceName = "_Total" + // }; + + // metric.ProcessorUsagePercentage = ProcessorTimeCounter.NextValue(); + // await Task.Delay(1000, cancellationToken).ConfigureAwait(false); + // } + + // metric.Timestamp = DateTime.Now; + // metric.ProcessorUsagePercentage = ProcessorTimeCounter?.NextValue(); + // return metric; + //} + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/ServiceHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/ServiceHandler.cs new file mode 100644 index 0000000..db3fb34 --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/ServiceHandler.cs @@ -0,0 +1,118 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using System.Management; +using System.Runtime.Versioning; +using System.ServiceProcess; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + public class ServiceHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + var result = new ServiceList(); + result.AddRange(GetServices()); + + await sender.SendAsync(result, cancellationToken); + } + } + + private static List GetServices() + { + var services = new List(); + + var serviceControllers = ServiceController.GetServices()?.OrderBy(s => s.DisplayName)?.ToList(); + if (serviceControllers is null || serviceControllers.Any() is false) throw new InvalidOperationException("SERVICE Collection NULL"); + + foreach (var sc in serviceControllers) + { + var status = sc.Status switch + { + ServiceControllerStatus.Stopped => Service.ServiceStatus.Stopped, + ServiceControllerStatus.StartPending => Service.ServiceStatus.StartPending, + ServiceControllerStatus.StopPending => Service.ServiceStatus.StopPending, + ServiceControllerStatus.Running => Service.ServiceStatus.Running, + ServiceControllerStatus.ContinuePending => Service.ServiceStatus.ContinuePending, + ServiceControllerStatus.PausePending => Service.ServiceStatus.PausePending, + ServiceControllerStatus.Paused => Service.ServiceStatus.Paused, + _ => Service.ServiceStatus.Unknown + }; + + var mode = sc.StartType switch + { + ServiceStartMode.Boot => Service.ServiceMode.Boot, + ServiceStartMode.System => Service.ServiceMode.System, + ServiceStartMode.Automatic => Service.ServiceMode.Automatic, + ServiceStartMode.Manual => Service.ServiceMode.Manual, + ServiceStartMode.Disabled => Service.ServiceMode.Disabled, + _ => Service.ServiceMode.Unknown, + }; + + var service = new Service + { + Name = sc.ServiceName?.Trim(), + Display = sc.DisplayName?.Trim(), + Status = status, + StartMode = mode + }; + + services.Add(service); + } + + // additional infos + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\cimv2"), + Query = new ObjectQuery("SELECT processid, name, description, pathname, startname, delayedautostart from win32_service") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from win32_service"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var services2 = new List(); + + using (collection) + { + foreach (ManagementObject @object in collection.Cast()) + { + var service2 = new Service(); + + var properties = @object.GetPropertyHashes(); + + service2.Name = @object.GetValue(properties, "name")?.Trim(); + service2.ProcessId = @object.GetValue(properties, "processid"); + service2.Description = @object.GetValue(properties, "description")?.Trim(); + service2.PathName = @object.GetValue(properties, "pathname")?.Trim(); + service2.Account = @object.GetValue(properties, "startname")?.Trim(); + service2.Delay = @object.GetValue(properties, "delayedautostart"); + + services2.Add(service2); + } + } + + if (services2.Any() is false) return services; + + foreach (var svc in services) + { + var map = services2.Where(p => p.Name == svc.Name).FirstOrDefault(); + + if (map is null) continue; + + svc.ProcessId = map.ProcessId; + svc.Description = map.Description; + svc.PathName = map.PathName; + svc.Account = map.Account; + svc.Delay = map.Delay; + } + + return services.OrderBy(x => x.Name).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/SessionHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/SessionHandler.cs new file mode 100644 index 0000000..c857127 --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/SessionHandler.cs @@ -0,0 +1,252 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + public class SessionHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + var result = new SessionList(); + result.AddRange(GetSessions()); + + await sender.SendAsync(result, cancellationToken); + } + } + + private static List GetSessions() + { + var query = NativeMethods.GetSessions(); + + var sessions = new List(); + + foreach (var s in NativeMethods.GetSessions()) + { + sessions.Add(new Session + { + Sid = s.SessionId.ToString(), + User = s.Username, + Type = s.Workstation, + Status = s.State.ToString(), + Remote = s.IPAddress + }); + } + + return sessions; + } + + private static partial class NativeMethods + { + //public const int WTS_CURRENT_SESSION = -1; + + [DllImport("wtsapi32.dll")] + static extern int WTSEnumerateSessions( + nint pServer, + [MarshalAs(UnmanagedType.U4)] int iReserved, + [MarshalAs(UnmanagedType.U4)] int iVersion, + ref nint pSessionInfo, + [MarshalAs(UnmanagedType.U4)] ref int iCount); + + [DllImport("Wtsapi32.dll")] + private static extern bool WTSQuerySessionInformation( + nint pServer, + int iSessionID, + WTS_INFO_CLASS oInfoClass, + out nint pBuffer, + out uint iBytesReturned); + + [DllImport("wtsapi32.dll")] + static extern void WTSFreeMemory( + nint pMemory); + + [StructLayout(LayoutKind.Sequential)] + private struct WTS_CLIENT_ADDRESS + { + public int iAddressFamily; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] + public byte[] bAddress; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WTS_SESSION_INFO + { + public int iSessionID; + [MarshalAs(UnmanagedType.LPStr)] + public string sWinsWorkstationName; + public WTS_CONNECTSTATE_CLASS oState; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WTS_CLIENT_DISPLAY + { + public int iHorizontalResolution; + public int iVerticalResolution; + //1 = The display uses 4 bits per pixel for a maximum of 16 colors. + //2 = The display uses 8 bits per pixel for a maximum of 256 colors. + //4 = The display uses 16 bits per pixel for a maximum of 2^16 colors. + //8 = The display uses 3-byte RGB values for a maximum of 2^24 colors. + //16 = The display uses 15 bits per pixel for a maximum of 2^15 colors. + public int iColorDepth; + } + + public enum WTS_CONNECTSTATE_CLASS + { + WTSActive, + WTSConnected, + WTSConnectQuery, + WTSShadow, + WTSDisconnected, + WTSIdle, + WTSListen, + WTSReset, + WTSDown, + WTSInit + } + + public enum WTS_INFO_CLASS + { + WTSInitialProgram, + WTSApplicationName, + WTSWorkingDirectory, + WTSOEMId, + WTSSessionId, + WTSUserName, + WTSWinStationName, + WTSDomainName, + WTSConnectState, + WTSClientBuildNumber, + WTSClientName, + WTSClientDirectory, + WTSClientProductId, + WTSClientHardwareId, + WTSClientAddress, + WTSClientDisplay, + WTSClientProtocolType, + WTSIdleTime, + WTSLogonTime, + WTSIncomingBytes, + WTSOutgoingBytes, + WTSIncomingFrames, + WTSOutgoingFrames, + WTSClientInfo, + WTSSessionInfo, + WTSConfigInfo, + WTSValidationInfo, + WTSSessionAddressV4, + WTSIsRemoteSession + } + + public class WTSSession + { + public int SessionId { get; set; } + public WTS_CONNECTSTATE_CLASS State { get; set; } + public string? Workstation { get; set; } + public string? IPAddress { get; set; } + public string? Username { get; set; } + public int HorizontalResolution { get; set; } + public int VerticalResolution { get; set; } + public int ColorDepth { get; set; } + public string? ClientApplicationDirectory { get; set; } + } + + public static IEnumerable GetSessions() + { + var sessions = new List(); + + var pServer = nint.Zero; + var pSessionInfo = nint.Zero; + + try + { + var count = 0; + var sessionCount = WTSEnumerateSessions(pServer, 0, 1, ref pSessionInfo, ref count); + var dataSize = (long)Marshal.SizeOf(typeof(WTS_SESSION_INFO)); + var current = (long)pSessionInfo; + + if (sessionCount <= 0) + return sessions; + + for (int i = 0; i < count; i++) + { + if (Marshal.PtrToStructure((nint)current, typeof(WTS_SESSION_INFO)) is not object sessionStructure) + continue; + + var sessionInfo = (WTS_SESSION_INFO)sessionStructure; + current += dataSize; + + var session = new WTSSession + { + SessionId = sessionInfo.iSessionID, + State = sessionInfo.oState, + Workstation = sessionInfo.sWinsWorkstationName, + }; + + var returned = (uint)0; + + // get terminal user address + var address = nint.Zero; + var clientAddress = new WTS_CLIENT_ADDRESS(); + + if (WTSQuerySessionInformation(pServer, sessionInfo.iSessionID, WTS_INFO_CLASS.WTSClientAddress, out address, out returned) == true) + { + if (Marshal.PtrToStructure(address, clientAddress.GetType()) is not object addressStructure) + break; + + clientAddress = (WTS_CLIENT_ADDRESS)addressStructure; + session.IPAddress = clientAddress.bAddress[2] + "." + clientAddress.bAddress[3] + "." + clientAddress.bAddress[4] + "." + clientAddress.bAddress[5]; + } + + // get terminal user name + if (WTSQuerySessionInformation(pServer, sessionInfo.iSessionID, WTS_INFO_CLASS.WTSUserName, out address, out returned) == true) + { + session.Username = Marshal.PtrToStringAnsi(address); + } + + // get terminal user domain name + if (WTSQuerySessionInformation(pServer, sessionInfo.iSessionID, WTS_INFO_CLASS.WTSDomainName, out address, out returned) == true) + { + session.Username = Marshal.PtrToStringAnsi(address) + @"\" + session.Username; + } + + // get terminal user display informations + var clientDisplay = new WTS_CLIENT_DISPLAY(); + + if (WTSQuerySessionInformation(pServer, sessionInfo.iSessionID, WTS_INFO_CLASS.WTSClientDisplay, out address, out returned) == true) + { + if (Marshal.PtrToStructure(address, clientDisplay.GetType()) is not object displayStructure) + break; + + clientDisplay = (WTS_CLIENT_DISPLAY)displayStructure; + session.HorizontalResolution = clientDisplay.iHorizontalResolution; + session.VerticalResolution = clientDisplay.iVerticalResolution; + session.ColorDepth = clientDisplay.iColorDepth; + } + + // get terminal user application directory + if (WTSQuerySessionInformation(pServer, sessionInfo.iSessionID, WTS_INFO_CLASS.WTSClientDirectory, out address, out returned) == true) + { + session.ClientApplicationDirectory = Marshal.PtrToStringAnsi(address); + } + + sessions.Add(session); + } + } + catch (Exception) + { + throw; + } + finally + { + WTSFreeMemory(pSessionInfo); + } + + return sessions; + } + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/SoftwareHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/SoftwareHandler.cs new file mode 100644 index 0000000..806c2d4 --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/SoftwareHandler.cs @@ -0,0 +1,119 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Microsoft.Win32; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Security.AccessControl; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + internal class SoftwareHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + var x64 = Task.Run(() => ApplicationRegistryQuery(RegistryView.Registry64), cancellationToken); + var x86 = Task.Run(() => ApplicationRegistryQuery(RegistryView.Registry32), cancellationToken); + + await Task.WhenAll(x64, x86).ConfigureAwait(false); + + var result = new ApplicationList(); + result.AddRange(x64.Result); + result.AddRange(x86.Result.Where(p => result.All(app => p.Name != app.Name && p.Version != app.Version))); + + await sender.SendAsync(result, cancellationToken); + } + } + + private static IEnumerable ApplicationRegistryQuery(RegistryView registryView) + { + using var registry = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, registryView); + + using var key = registry.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\", RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey); + if (key is null) throw new NullReferenceException(nameof(key)); + + var apps = new List(); + + var architecture = registryView switch + { + RegistryView.Registry32 => Architecture.X86, + _ => Architecture.X64 + }; + + foreach (string name in key.GetSubKeyNames()) + { + using var query = key.OpenSubKey(name); + if (query is null) continue; + + var app = new Application + { + Architecture = architecture + }; + + if (query.GetValue("DisplayName")?.ToString()?.Trim() is string displayName && string.IsNullOrWhiteSpace(displayName) is false) + { + app.Name = displayName; + } + + if (query.GetValue("Publisher")?.ToString()?.Trim() is string publisher && string.IsNullOrWhiteSpace(publisher) is false) + { + app.Publisher = publisher; + } + + if (query.GetValue("DisplayVersion")?.ToString()?.Trim() is string version && string.IsNullOrWhiteSpace(version) is false) + { + app.Version = version; + } + + if (query.GetValue("InstallLocation")?.ToString()?.Trim() is string location && string.IsNullOrWhiteSpace(location) is false) + { + app.Location = location; + } + + if (query.GetValue("InstallSource")?.ToString()?.Trim() is string source && string.IsNullOrWhiteSpace(source) is false) + { + app.Source = source; + } + + if (query.GetValue("UninstallString")?.ToString()?.Trim() is string uninstall && string.IsNullOrWhiteSpace(uninstall) is false) + { + app.Uninstall = uninstall; + } + + if (app.Uninstall is null) + { + if (query.GetValue("UninstallString_Hidden")?.ToString()?.Trim() is string uninstall2 && string.IsNullOrWhiteSpace(uninstall2) is false) + { + app.Uninstall = uninstall2; + } + } + + if (query.GetValue("InstallDate")?.ToString()?.Trim() is string installDate) + { + if (DateTime.TryParseExact(installDate, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime valid)) + { + app.InstallDate = valid; + } + + if (app.InstallDate is null) + { + if (DateTime.TryParseExact(installDate, "dd.MM.yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime valid2)) + { + app.InstallDate = valid2; + } + } + } + + if (app.Name is not null) + { + apps.Add(app); + } + } + + return apps; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/StoragePoolHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/StoragePoolHandler.cs new file mode 100644 index 0000000..84b5506 --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/StoragePoolHandler.cs @@ -0,0 +1,308 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using System.Management; +using System.Runtime.Versioning; +using static Insight.Agent.Messages.PhysicalDisk; +using static Insight.Agent.Messages.StoragePool; +using static Insight.Agent.Messages.VirtualDisk; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + public class StoragePoolHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + var result = new StoragePoolList(); + result.AddRange(GetStoragePool()); + + await sender.SendAsync(result, cancellationToken); + } + } + + private static List GetStoragePool() + { + if (Environment.OSVersion.Version.Major < 6 || Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor < 2) + { + throw new PlatformNotSupportedException(); + } + + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\microsoft\windows\storage"), + Query = new ObjectQuery("select objectid, uniqueid, name, friendlyname, resiliencysettingnamedefault, isprimordial, isreadonly, isclustered, size, allocatedsize, logicalsectorsize, operationalstatus, healthstatus from msft_storagepool") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from msft_storagepool"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var pools = new List(); + + using (collection) + { + foreach (ManagementObject @object in collection.Cast()) + { + var pool = new StoragePool(); + + var properties = @object.GetPropertyHashes(); + + pool.UniqueId = @object.GetValue(properties, "uniqueid")?.Trim(); + pool.Name = @object.GetValue(properties, "name")?.Trim(); + pool.FriendlyName = @object.GetValue(properties, "friendlyname")?.Trim(); + + if (@object.TryGetValue(properties, "operationalstatus", out var operationals) && operationals is not null) + { + pool.States = operationals.Select(p => (StoragePool.OperationalState)p).ToList(); + } + + pool.Health = (StoragePool.HealthState)@object.GetValue(properties, "healthstatus"); + pool.RetireMissingPhysicalDisks = (RetireMissingPhysicalDisksEnum)@object.GetValue(properties, "retiremissingphysicaldisks"); + pool.Resiliency = @object.GetValue(properties, "resiliencysettingnamedefault")?.Trim(); + pool.IsPrimordial = @object.GetValue(properties, "isprimordial"); + pool.IsReadOnly = @object.GetValue(properties, "isreadonly"); + pool.IsClustered = @object.GetValue(properties, "isclustered"); + pool.Size = @object.GetValue(properties, "size"); + pool.AllocatedSize = @object.GetValue(properties, "allocatedsize"); + pool.SectorSize = @object.GetValue(properties, "logicalsectorsize"); + + if (@object.GetValue(properties, "objectid") is string objectId) + { + pool.PhysicalDisks = QueryPhysicalDisksByStoragePool(objectId); + pool.VirtualDisks = QueryVirtualDisksByStoragePool(objectId); + } + + pools.Add(pool); + } + } + + return pools; + } + + private static List GetPhysicalDisks() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\microsoft\windows\storage"), + Query = new ObjectQuery("select objectid, uniqueid, name, friendlyname from msft_physicaldisk") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from msft_physicaldisk"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var disks = new List(); + + using (collection) + { + foreach (ManagementObject @object in collection.Cast()) + { + var disk = new PhysicalDisk(); + + var properties = @object.GetPropertyHashes(); + + disk.UniqueId = @object.GetValue(properties, "uniqueid")?.Trim(); + disk.FriendlyName = @object.GetValue(properties, "friendlyname")?.Trim(); + disk.Manufacturer = @object.GetValue(properties, "manufacturer")?.Trim(); + disk.Model = @object.GetValue(properties, "model")?.Trim(); + disk.MediaType = @object.GetValue(properties, "mediatype"); + disk.BusType = @object.GetValue(properties, "bustype"); + + if (@object.TryGetValue(properties, "operationalstatus", out var operationals) && operationals is not null) + { + disk.States = operationals.Select(p => (PhysicalDisk.OperationalState)p).ToList(); + } + + disk.Health = (PhysicalDisk.HealthState)@object.GetValue(properties, "healthstatus"); + + if (@object.TryGetValue(properties, "supportedusages", out var supportedusages) && supportedusages is not null) + { + disk.SupportedUsages = supportedusages.Select(p => (SupportedUsagesEnum)p).ToList(); + } + + disk.Usage = @object.GetValue(properties, "usage"); + disk.PhysicalLocation = @object.GetValue(properties, "physicallocation")?.Trim(); + disk.SerialNumber = @object.GetValue(properties, "serialnumber")?.Trim(); + disk.FirmwareVersion = @object.GetValue(properties, "firmwareversion")?.Trim(); + disk.Size = @object.GetValue(properties, "size"); + disk.AllocatedSize = @object.GetValue(properties, "allocatedsize"); + disk.LogicalSectorSize = @object.GetValue(properties, "logicalsectorsize"); + disk.PhysicalSectorSize = @object.GetValue(properties, "physicalsectorsize"); + disk.VirtualDiskFootprint = @object.GetValue(properties, "virtualdiskfootprint"); + + disks.Add(disk); + } + } + + return disks; + } + + private static List GetVirtualDisks() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\microsoft\windows\storage"), + Query = new ObjectQuery("select objectid, uniqueid, name, friendlyname, access, provisioningtype, physicaldiskredundancy, resiliencysettingname, isdeduplicationenabled, issnapshot, operationalstatus, healthstatus, size, allocatedsize, footprintonpool, readcachesize, writecachesize from msft_virtualdisk") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from msft_virtualdisk"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var disks = new List(); + + using (collection) + { + foreach (ManagementObject @object in collection.Cast()) + { + var disk = new VirtualDisk(); + + var properties = @object.GetPropertyHashes(); + + disk.UniqueId = @object.GetValue(properties, "uniqueid")?.Trim(); + disk.Name = @object.GetValue(properties, "name")?.Trim(); + disk.FriendlyName = @object.GetValue(properties, "friendlyname")?.Trim(); + disk.AccessType = (AccessTypeEnum)@object.GetValue(properties, "access"); + disk.ProvisioningType = (ProvisioningTypeEnum)@object.GetValue(properties, "provisioningtype"); + disk.PhysicalDiskRedundancy = @object.GetValue(properties, "physicaldiskredundancy"); + disk.ResiliencySettingName = @object.GetValue(properties, "resiliencysettingname")?.Trim(); + disk.Deduplication = @object.GetValue(properties, "isdeduplicationenabled"); + disk.IsSnapshot = @object.GetValue(properties, "issnapshot"); + + if (@object.TryGetValue(properties, "operationalstatus", out var operationals) && operationals is not null) + { + disk.States = operationals.Select(p => (VirtualDisk.OperationalState)p).ToList(); + } + + disk.Health = (VirtualDisk.HealthState)@object.GetValue(properties, "healthstatus"); + disk.Size = @object.GetValue(properties, "size"); + disk.AllocatedSize = @object.GetValue(properties, "allocatedsize"); + disk.FootprintOnPool = @object.GetValue(properties, "footprintonpool"); + disk.ReadCacheSize = @object.GetValue(properties, "readcachesize"); + disk.WriteCacheSize = @object.GetValue(properties, "writecachesize"); + + disks.Add(disk); + } + } + + return disks; + } + + private static List QueryPhysicalDisksByStoragePool(string storagePoolObjectId) + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\microsoft\windows\storage"), + Query = new ObjectQuery("ASSOCIATORS OF {MSFT_StoragePool.ObjectId=\"" + Helpers.EscapeWql(storagePoolObjectId) + "\"} WHERE AssocClass = MSFT_StoragePoolToPhysicalDisk") + }; + + if (searcher.TryGet(out var collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + + var disks = new List(); + + using (collection) + { + foreach (ManagementObject @object in collection) + { + var disk = new PhysicalDisk(); + + var properties = @object.GetPropertyHashes(); + + disk.UniqueId = @object.GetValue(properties, "uniqueid")?.Trim(); + disk.DeviceId = @object.GetValue(properties, "deviceid")?.Trim(); + disk.FriendlyName = @object.GetValue(properties, "friendlyname")?.Trim(); + disk.Manufacturer = @object.GetValue(properties, "manufacturer")?.Trim(); + disk.Model = @object.GetValue(properties, "model")?.Trim(); + disk.MediaType = @object.GetValue(properties, "mediatype"); + disk.BusType = @object.GetValue(properties, "bustype"); + + if (@object.TryGetValue(properties, "operationalstatus", out var operationals) && operationals is not null) + { + disk.States = operationals.Select(p => (PhysicalDisk.OperationalState)p).ToList(); + } + + disk.Health = (PhysicalDisk.HealthState)@object.GetValue(properties, "healthstatus"); + + if (@object.TryGetValue(properties, "supportedusages", out var supportedusages) && supportedusages is not null) + { + disk.SupportedUsages = supportedusages.Select(p => (SupportedUsagesEnum)p).ToList(); + } + + disk.Usage = @object.GetValue(properties, "usage"); + disk.PhysicalLocation = @object.GetValue(properties, "physicallocation")?.Trim(); + disk.SerialNumber = @object.GetValue(properties, "serialnumber")?.Trim(); + disk.FirmwareVersion = @object.GetValue(properties, "firmwareversion")?.Trim(); + disk.Size = @object.GetValue(properties, "size"); + disk.AllocatedSize = @object.GetValue(properties, "allocatedsize"); + disk.LogicalSectorSize = @object.GetValue(properties, "logicalsectorsize"); + disk.PhysicalSectorSize = @object.GetValue(properties, "physicalsectorsize"); + disk.VirtualDiskFootprint = @object.GetValue(properties, "virtualdiskfootprint"); + + disks.Add(disk); + } + } + + return disks; + } + + private static List QueryVirtualDisksByStoragePool(string storagePoolObjectId) + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\microsoft\windows\storage"), + Query = new ObjectQuery("ASSOCIATORS OF {MSFT_StoragePool.ObjectId=\"" + Helpers.EscapeWql(storagePoolObjectId) + "\"} WHERE AssocClass = MSFT_StoragePoolToVirtualDisk") + }; + + if (searcher.TryGet(out var collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + + var disks = new List(); + + using (collection) + { + foreach (ManagementObject @object in collection) + { + var disk = new VirtualDisk(); + + var properties = @object.GetPropertyHashes(); + + disk.UniqueId = @object.GetValue(properties, "uniqueid")?.Trim(); + disk.Name = @object.GetValue(properties, "name")?.Trim(); + disk.FriendlyName = @object.GetValue(properties, "friendlyname")?.Trim(); + disk.AccessType = (AccessTypeEnum)@object.GetValue(properties, "access"); + disk.ProvisioningType = (ProvisioningTypeEnum)@object.GetValue(properties, "provisioningtype"); + disk.PhysicalDiskRedundancy = @object.GetValue(properties, "physicaldiskredundancy"); + disk.ResiliencySettingName = @object.GetValue(properties, "resiliencysettingname")?.Trim(); + disk.Deduplication = @object.GetValue(properties, "isdeduplicationenabled"); + disk.IsSnapshot = @object.GetValue(properties, "issnapshot"); + + if (@object.TryGetValue(properties, "operationalstatus", out var operationals) && operationals is not null) + { + disk.States = operationals.Select(p => (VirtualDisk.OperationalState)p).ToList(); + } + + disk.Health = (VirtualDisk.HealthState)@object.GetValue(properties, "healthstatus"); + disk.Size = @object.GetValue(properties, "size"); + disk.AllocatedSize = @object.GetValue(properties, "allocatedsize"); + disk.FootprintOnPool = @object.GetValue(properties, "footprintonpool"); + disk.ReadCacheSize = @object.GetValue(properties, "readcachesize"); + disk.WriteCacheSize = @object.GetValue(properties, "writecachesize"); + + disks.Add(disk); + } + } + + return disks; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/SystemInfoHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/SystemInfoHandler.cs new file mode 100644 index 0000000..6f9ee19 --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/SystemInfoHandler.cs @@ -0,0 +1,175 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Microsoft.Win32; +using System.Collections; +using System.Management; +using System.Runtime.Versioning; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + public class SystemInfoHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + await sender.SendAsync(GetSystem(), cancellationToken); + } + } + + private static SystemInfo GetSystem() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\cimv2"), + Query = new ObjectQuery("select lastbootuptime, localdatetime, numberofprocesses from win32_operatingsystem") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from win32_operatingsystem"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var system = new SystemInfo(); + + using (collection) + { + foreach (ManagementObject @object in collection.Cast()) + { + var properties = @object.GetPropertyHashes(); + + if (@object.TryGetValue(properties, "lastbootuptime", out var lastbootuptime)) + { + system.LastBootUpTime = ManagementDateTimeConverter.ToDateTime(lastbootuptime?.ToString()); + } + + if (@object.TryGetValue(properties, "localdatetime", out var localdatetime)) + { + system.LocalDateTime = ManagementDateTimeConverter.ToDateTime(localdatetime?.ToString()); + } + + system.Processes = @object.GetValue(properties, "numberofprocesses"); + + break; + } + } + + system.License = GetWindowsProductKeyFromRegistry(); + + return system; + } + + private static string GetWindowsProductKeyFromRegistry() + { + var localKey = + RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, Environment.Is64BitOperatingSystem + ? RegistryView.Registry64 + : RegistryView.Registry32); + + var registryKeyValue = localKey.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion")?.GetValue("DigitalProductId"); + if (registryKeyValue == null) + return "Failed to get DigitalProductId from registry"; + var digitalProductId = (byte[])registryKeyValue; + localKey.Close(); + var isWin8OrUp = + Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor >= 2 + || + Environment.OSVersion.Version.Major > 6; + return GetWindowsProductKeyFromDigitalProductId(digitalProductId, + isWin8OrUp ? DigitalProductIdVersion.Windows8AndUp : DigitalProductIdVersion.UpToWindows7); + } + + private static string GetWindowsProductKeyFromDigitalProductId(byte[] digitalProductId, DigitalProductIdVersion digitalProductIdVersion) + { + + var productKey = digitalProductIdVersion == DigitalProductIdVersion.Windows8AndUp + ? DecodeProductKeyWin8AndUp(digitalProductId) + : DecodeProductKey(digitalProductId); + return productKey; + } + + private static string DecodeProductKey(byte[] digitalProductId) + { + const int keyStartIndex = 52; + const int keyEndIndex = keyStartIndex + 15; + var digits = new[] + { + 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'P', 'Q', 'R', + 'T', 'V', 'W', 'X', 'Y', '2', '3', '4', '6', '7', '8', '9', + }; + const int decodeLength = 29; + const int decodeStringLength = 15; + var decodedChars = new char[decodeLength]; + var hexPid = new ArrayList(); + for (var i = keyStartIndex; i <= keyEndIndex; i++) + { + hexPid.Add(digitalProductId[i]); + } + for (var i = decodeLength - 1; i >= 0; i--) + { + // Every sixth char is a separator. + if ((i + 1) % 6 == 0) + { + decodedChars[i] = '-'; + } + else + { + // Do the actual decoding. + var digitMapIndex = 0; + for (var j = decodeStringLength - 1; j >= 0; j--) + { + var byteValue = digitMapIndex << 8 | (byte)hexPid[j]; + hexPid[j] = (byte)(byteValue / 24); + digitMapIndex = byteValue % 24; + decodedChars[i] = digits[digitMapIndex]; + } + } + } + return new string(decodedChars); + } + + private static string DecodeProductKeyWin8AndUp(byte[] digitalProductId) + { + var key = string.Empty; + const int keyOffset = 52; + var isWin8 = (byte)(digitalProductId[66] / 6 & 1); + digitalProductId[66] = (byte)(digitalProductId[66] & 0xf7 | (isWin8 & 2) * 4); + + const string digits = "BCDFGHJKMPQRTVWXY2346789"; + var last = 0; + for (var i = 24; i >= 0; i--) + { + var current = 0; + for (var j = 14; j >= 0; j--) + { + current = current * 256; + current = digitalProductId[j + keyOffset] + current; + digitalProductId[j + keyOffset] = (byte)(current / 24); + current = current % 24; + last = current; + } + key = digits[current] + key; + } + + var keypart1 = key.Substring(1, last); + var keypart2 = key.Substring(last + 1, key.Length - (last + 1)); + key = keypart1 + "N" + keypart2; + + for (var i = 5; i < key.Length; i += 6) + { + key = key.Insert(i, "-"); + } + + return key; + } + + private enum DigitalProductIdVersion + { + UpToWindows7, + Windows8AndUp + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/UpdateHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/UpdateHandler.cs new file mode 100644 index 0000000..287c181 --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/UpdateHandler.cs @@ -0,0 +1,130 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using System.Runtime.Versioning; +using System.Text.RegularExpressions; +using WUApiLib; +using static Insight.Agent.Messages.Update; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + public class UpdateHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + await sender.SendAsync(GetUpdates(), cancellationToken); + } + } + + private static UpdateList GetUpdates() + { + return new UpdateList + { + Installed = QueryInstalledUpdates(), + Pending = QueryPendingUpdates() + }; + } + + private static List QueryInstalledUpdates() + { + var updates = new List(); + + var session = new UpdateSessionClass(); + var searcher = session.CreateUpdateSearcher(); + searcher.Online = false; + + var count = searcher.GetTotalHistoryCount(); + var result = searcher.QueryHistory(0, count); + + foreach (IUpdateHistoryEntry wupdate in result) + { + var update = new Update + { + Id = wupdate.UpdateIdentity.UpdateID, + Date = wupdate.Date, + Name = wupdate.Title, + Description = wupdate.Description, + Result = wupdate.ResultCode switch + { + OperationResultCode.orcNotStarted => OsUpdateResultCodeEnum.NotStarted, + OperationResultCode.orcInProgress => OsUpdateResultCodeEnum.InProgress, + OperationResultCode.orcSucceeded => OsUpdateResultCodeEnum.Succeeded, + OperationResultCode.orcSucceededWithErrors => OsUpdateResultCodeEnum.SucceededWithErrors, + OperationResultCode.orcFailed => OsUpdateResultCodeEnum.Failed, + OperationResultCode.orcAborted => OsUpdateResultCodeEnum.Aborted, + _ => null + }, + SupportUrl = wupdate.SupportUrl, + }; + + try + { + var rx = new Regex(@"KB(\d+)"); + update.Hotfix = rx.Match(wupdate.Title).Value; + } + catch (Exception) + { + + } + + updates.Add(update); + } + + return updates; + } + + private static List QueryPendingUpdates() + { + var updates = new List(); + + var session = new UpdateSessionClass(); + var searcher = session.CreateUpdateSearcher(); + searcher.Online = true; + + var result = searcher.Search("IsInstalled=0"); + + foreach (IUpdate wupdate in result.Updates) + { + var update = new Update + { + Id = wupdate.Identity.UpdateID, + Type = wupdate.Type switch + { + UpdateType.utSoftware => OsUpdateTypeEnum.Software, + UpdateType.utDriver => OsUpdateTypeEnum.Driver, + _ => null + }, + Date = wupdate.LastDeploymentChangeTime, + Name = wupdate.Title, + Description = wupdate.Description, + SupportUrl = wupdate.SupportUrl, + Size = wupdate.MaxDownloadSize, + IsDownloaded = wupdate.IsDownloaded, + CanRequestUserInput = wupdate.InstallationBehavior.CanRequestUserInput, + RebootBehavior = wupdate.InstallationBehavior.RebootBehavior switch + { + InstallationRebootBehavior.irbNeverReboots => OsUpdateRebootBehaviorEnum.NeverReboots, + InstallationRebootBehavior.irbAlwaysRequiresReboot => OsUpdateRebootBehaviorEnum.AlwaysRequiresReboot, + InstallationRebootBehavior.irbCanRequestReboot => OsUpdateRebootBehaviorEnum.CanRequestReboot, + _ => null + }, + }; + + if (wupdate.KBArticleIDs.Count > 0) + { + foreach (var id in wupdate.KBArticleIDs) + { + update.Hotfix = $"KB{id}"; + break; + } + } + + updates.Add(update); + } + + return updates; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/UserHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/UserHandler.cs new file mode 100644 index 0000000..b232e3a --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/UserHandler.cs @@ -0,0 +1,188 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using System.Management; +using System.Runtime.Versioning; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + public class UserHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + var result = new UserList(); + result.AddRange(GetUsers()); + + await sender.SendAsync(result, cancellationToken); + } + } + + private static List GetUsers() + { + var users = QueryUsers(); + var groups = GetGroups(); + var usergrouping = QueryUserGroupMaps(); + + foreach (var u in users) + { + u.Groups = new List(); + + foreach (var ug in usergrouping.Where(ug => ug.UserDomain == u.Domain && ug.UserName == u.Name)) + { + var grps = groups.Where(g => g.Domain == ug.GroupDomain && g.Name == ug.GroupName); + + if (grps is not null) + { + u.Groups.AddRange(grps); + } + } + } + + return users; + } + + private static List GetGroups() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\cimv2"), + Query = new ObjectQuery("select sid, domain, name, description, localaccount from win32_group") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from win32_group"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var groups = new List(); + + using (collection) + { + foreach (ManagementObject @object in collection) + { + var group = new Group(); + + var properties = @object.GetPropertyHashes(); + + group.Sid = @object.GetValue(properties, "sid")?.Trim(); + group.Domain = @object.GetValue(properties, "domain")?.Trim(); + group.Name = @object.GetValue(properties, "name")?.Trim(); + group.Description = @object.GetValue(properties, "description")?.Trim(); + group.LocalAccount = @object.GetValue(properties, "localaccount"); + + groups.Add(group); + } + } + + return groups.OrderBy(x => x.Name)?.ToList(); + } + + private static List QueryUsers() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\cimv2"), + Query = new ObjectQuery("select sid, name, fullname, description, domain, localaccount, disabled, lockout, status, passwordchangeable, passwordexpires, passwordrequired from win32_useraccount") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from win32_useraccount"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var users = new List(); + + using (collection) + { + foreach (ManagementObject @object in collection) + { + var user = new User(); + + var properties = @object.GetPropertyHashes(); + + user.Sid = @object.GetValue(properties, "sid")?.Trim(); + user.Name = @object.GetValue(properties, "name")?.Trim(); + user.FullName = @object.GetValue(properties, "fullname")?.Trim(); + user.Description = @object.GetValue(properties, "description")?.Trim(); + user.Domain = @object.GetValue(properties, "domain")?.Trim(); + user.LocalAccount = @object.GetValue(properties, "localaccount"); + user.Disabled = @object.GetValue(properties, "disabled"); + user.Lockout = @object.GetValue(properties, "lockout"); + user.Status = @object.GetValue(properties, "status")?.Trim(); + user.PasswordChangeable = @object.GetValue(properties, "passwordchangeable"); + user.PasswordExpires = @object.GetValue(properties, "passwordexpires"); + user.PasswordRequired = @object.GetValue(properties, "passwordrequired"); + + users.Add(user); + } + } + + return users; + } + + private static List QueryUserGroupMaps() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\cimv2"), + Query = new ObjectQuery("select groupcomponent, partcomponent from win32_groupuser") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from win32_groupuser"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var usergroups = new List(); + + using (collection) + { + foreach (ManagementObject @object in collection) + { + var usergroup = new UserGroupMap(); + + var properties = @object.GetPropertyHashes(); + + var raw = @object.GetValue(properties, "groupcomponent"); + var split = raw?.Split(".Domain=")[1]?.Split(",Name="); + + if (split is not null && split.Length > 1) + { + usergroup.GroupDomain = split[0].TrimStart('"').TrimEnd('"'); + usergroup.GroupName = split[1].TrimStart('"').TrimEnd('"'); + } + + raw = @object.GetValue(properties, "partcomponent"); + split = raw?.Split(".Domain=")[1]?.Split(",Name="); + + if (split is not null && split.Length > 1) + { + usergroup.UserDomain = split[0].TrimStart('"').TrimEnd('"'); + usergroup.UserName = split[1].TrimStart('"').TrimEnd('"'); + } + + usergroups.Add(usergroup); + } + } + + return usergroups; + } + + private class UserGroupMap + { + public string? GroupDomain { get; set; } + public string? GroupName { get; set; } + + public string? UserDomain { get; set; } + public string? UserName { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/VideocardHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/VideocardHandler.cs new file mode 100644 index 0000000..0e8f1bb --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/VideocardHandler.cs @@ -0,0 +1,64 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using System.Management; +using System.Runtime.Versioning; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + public class VideocardHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + var result = new VideocardList(); + result.AddRange(GetVideocards()); + + await sender.SendAsync(result, cancellationToken); + } + } + + private static List GetVideocards() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\cimv2"), + Query = new ObjectQuery("select deviceid, name, adapterram, driverdate, driverversion from win32_videocontroller") + }; + + if (searcher.TryGet(out var collection) is false) + { + searcher.Query = new ObjectQuery("select * from win32_videocontroller"); + + if (searcher.TryGet(out collection) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var videocards = new List(); + + using (collection) + { + foreach (ManagementObject @object in collection.Cast()) + { + var videocard = new Videocard(); + + var properties = @object.GetPropertyHashes(); + + videocard.DeviceId = @object.GetValue(properties, "deviceid")?.Trim(); + videocard.Model = @object.GetValue(properties, "name")?.Trim(); + + if (@object.TryGetValue(properties, "driverdate", out var driverdate)) + { + videocard.DriverDate = ManagementDateTimeConverter.ToDateTime(driverdate?.ToString()); + } + + videocard.DriverVersion = @object.GetValue(properties, "driverversion")?.Trim(); + + videocards.Add(videocard); + } + } + + return videocards; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Network/Handlers/VirtualMaschineHandler.cs b/src/Agent/Insight.Agent/Network/Handlers/VirtualMaschineHandler.cs new file mode 100644 index 0000000..a6cc0ff --- /dev/null +++ b/src/Agent/Insight.Agent/Network/Handlers/VirtualMaschineHandler.cs @@ -0,0 +1,359 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using System.Management; +using System.Runtime.Versioning; +using static Insight.Agent.Messages.VirtualMaschine; +using static Insight.Agent.Messages.VirtualMaschineConfiguration; + +namespace Insight.Agent.Network.Handlers +{ + [SupportedOSPlatform("windows")] + public class VirtualMaschineHandler : IAgentMessageHandler + { + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is GetInventory) + { + var result = new VirtualMaschineList(); + result.AddRange(GetVirtualMaschines()); + + await sender.SendAsync(result, cancellationToken); + } + } + + private static List GetVirtualMaschines() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\virtualization\v2"), + Query = new ObjectQuery("select * msvm_computersystem") + }; + + if (searcher.TryGet(out var computersystems) is false) + { + searcher.Query = new ObjectQuery("select * from msvm_computersystem"); + + if (searcher.TryGet(out computersystems) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var vms = new List(); + + using (computersystems) + { + foreach (ManagementObject cs in computersystems.Cast()) + { + var vm = new VirtualMaschine(); + + var csProperties = cs.GetPropertyHashes(); + + var vmId = cs.GetValue(csProperties, "name")?.Trim(); + + if (Guid.TryParse(vmId, out var vmGuid) is false) continue; + + vm.Id = vmGuid; + vm.ProcessId = cs.GetValue(csProperties, "processid"); + vm.Caption = cs.GetValue(csProperties, "caption")?.Trim(); + vm.Name = cs.GetValue(csProperties, "elementname")?.Trim(); + vm.Enabled = (EnabledEnum)cs.GetValue(csProperties, "enabledstate"); + vm.EnabledDefault = (EnabledDefaultEnum)cs.GetValue(csProperties, "enableddefault"); + vm.HealthState = (HealthStatusEnum)cs.GetValue(csProperties, "healthstate"); + vm.Status = cs.GetValue(csProperties, "status")?.Trim(); + vm.OnTime = cs.GetValue(csProperties, "ontimeinmilliseconds"); + vm.ReplicationMode = cs.GetValue(csProperties, "replicationmode"); + vm.ReplicationState = (ReplicationStateEnum)cs.GetValue(csProperties, "replicationstate"); + vm.ReplicationHealth = (ReplicationHealthEnum)cs.GetValue(csProperties, "replicationhealth"); + + if (cs.TryGetValue(csProperties, "installdate", out var installdate)) + { + vm.InstallDate = ManagementDateTimeConverter.ToDateTime(installdate?.ToString()); + } + + if (cs.TryGetValue(csProperties, "timeoflastconfigurationchange", out var timeoflastconfigurationchange)) + { + vm.TimeOfLastConfigurationChange = ManagementDateTimeConverter.ToDateTime(timeoflastconfigurationchange?.ToString()); + } + + if (cs.TryGetValue(csProperties, "timeoflaststatechange", out var timeoflaststatechange)) + { + vm.TimeOfLastStateChange = ManagementDateTimeConverter.ToDateTime(timeoflaststatechange?.ToString()); + } + + if (cs.TryGetValue(csProperties, "lastreplicationtime", out var lastreplicationtime)) + { + vm.LastReplicationTime = ManagementDateTimeConverter.ToDateTime(lastreplicationtime?.ToString()); + } + + var summaryinformation = cs.GetRelated("msvm_summaryinformation"); + using (summaryinformation) + { + foreach (ManagementObject si in summaryinformation.Cast()) + { + var siProperties = si.GetPropertyHashes(); + + vm.Notes = si.GetValue(siProperties, "Notes"); + vm.ConfigurationVersion = si.GetValue(siProperties, "Version"); + vm.IntegrationServicesVersionState = (IntegrationServicesVersionStateEnum)si.GetValue(siProperties, "IntegrationServicesVersionState"); + vm.GuestOperatingSystem = si.GetValue(siProperties, "GuestOperatingSystem"); + vm.NumberOfProcessors = si.GetValue(siProperties, "NumberOfProcessors"); + vm.ProcessorLoad = si.GetValue(siProperties, "ProcessorLoad"); + vm.MemoryAvailable = si.GetValue(siProperties, "MemoryAvailable"); + vm.MemoryUsage = si.GetValue(siProperties, "MemoryUsage"); + } + } + + var virtualSystemSettingData = cs.GetRelated("Msvm_VirtualSystemSettingData"); + using (virtualSystemSettingData) + { + var configs = new List(); + + foreach (ManagementObject vssd in virtualSystemSettingData.Cast()) + { + var vmc = new VirtualMaschineConfiguration(); + + var vssdProperties = vssd.GetPropertyHashes(); + + var vmcId = vssd.GetValue(vssdProperties, "ConfigurationID")?.Trim(); + + if (Guid.TryParse(vmcId, out var vmcGuid) is false) continue; + + vmc.Id = vmcGuid.ToString(); + + vmc.Type = vssd.GetValue(vssdProperties, "VirtualSystemType"); + vmc.Name = vssd.GetValue(vssdProperties, "ElementName"); + + if (vssd.TryGetValue(vssdProperties, "CreationTime", out var creationtime)) + { + vmc.CreationTime = ManagementDateTimeConverter.ToDateTime(creationtime?.ToString()); + } + + vmc.Generation = vssd.GetValue(vssdProperties, "VirtualSystemSubType"); + vmc.Architecture = vssd.GetValue(vssdProperties, "Architecture"); + vmc.AutomaticStartupAction = (AutomaticStartupActionEnum)vssd.GetValue(vssdProperties, "AutomaticStartupAction"); + vmc.AutomaticShutdownAction = (AutomaticShutdownActionEnum)vssd.GetValue(vssdProperties, "AutomaticShutdownAction"); + vmc.AutomaticRecoveryAction = (AutomaticRecoveryActionEnum)vssd.GetValue(vssdProperties, "AutomaticRecoveryAction"); + vmc.AutomaticSnapshotsEnabled = vssd.GetValue(vssdProperties, "AutomaticSnapshotsEnabled"); + + //if (vssd.TryGetValue(vssdProperties, "AutomaticStartupActionDelay", out var automaticstartupactiondelay)) + //{ + // vmc.CreationTime = ManagementDateTimeConverter.ToDateTime(automaticstartupactiondelay?.ToString()); + //} + + vmc.BaseBoardSerialNumber = vssd.GetValue(vssdProperties, "BaseBoardSerialNumber"); + vmc.BIOSGUID = vssd.GetValue(vssdProperties, "BIOSGUID"); + vmc.BIOSSerialNumber = vssd.GetValue(vssdProperties, "BIOSSerialNumber"); + vmc.BootOrder = vssd.GetValue(vssdProperties, "BootOrder"); + vmc.ConfigurationDataRoot = vssd.GetValue(vssdProperties, "ConfigurationDataRoot"); + vmc.ConfigurationFile = vssd.GetValue(vssdProperties, "ConfigurationFile"); + vmc.GuestStateDataRoot = vssd.GetValue(vssdProperties, "GuestStateDataRoot"); + vmc.GuestStateFile = vssd.GetValue(vssdProperties, "GuestStateFile"); + vmc.SnapshotDataRoot = vssd.GetValue(vssdProperties, "SnapshotDataRoot"); + vmc.SuspendDataRoot = vssd.GetValue(vssdProperties, "SuspendDataRoot"); + vmc.SwapFileDataRoot = vssd.GetValue(vssdProperties, "SwapFileDataRoot"); + vmc.SecureBootEnabled = vssd.GetValue(vssdProperties, "SecureBootEnabled"); + vmc.IsAutomaticSnapshot = vssd.GetValue(vssdProperties, "IsAutomaticSnapshot"); + vmc.Notes = vssd.GetValue(vssdProperties, "Notes"); + + if (vssd.GetValue(vssdProperties, "Parent") is string parent) + { + using var vmcp = new ManagementObject(parent); + vmcp.Get(); + + if (Guid.TryParse(vmcp["ConfigurationID"]?.ToString(), out var parentGuid) is false) continue; + vmc.ParentId = parentGuid.ToString(); + } + + //var storageallocationsettingdata = cs.GetRelated("Msvm_StorageAllocationSettingData"); + //using (storageallocationsettingdata) + //{ + + //} + + configs.Add(vmc); + } + + vm.Configurations = configs.GroupBy(p => p.Id).Select(p => p.First()).ToList(); + } + + vms.Add(vm); + } + } + + return vms; + } + + private static List QueryVirtualMaschines0() + { + using var searcher = new ManagementObjectSearcher + { + Scope = new ManagementScope(@"root\virtualization\v2"), + Query = new ObjectQuery("select * msvm_computersystem") + }; + + if (searcher.TryGet(out var computersystems) is false) + { + searcher.Query = new ObjectQuery("select * from msvm_computersystem"); + + if (searcher.TryGet(out computersystems) is false) throw new InvalidOperationException("WMI Collection NULL"); + } + + var vms = new List(); + + using (computersystems) + { + foreach (ManagementObject cs in computersystems.Cast()) + { + var vm = new VirtualMaschine(); + + var csProperties = cs.GetPropertyHashes(); + + var vmId = cs.GetValue(csProperties, "name")?.Trim(); + + if (Guid.TryParse(vmId, out var vmGuid) is false) continue; + + vm.Id = vmGuid; + vm.ProcessId = cs.GetValue(csProperties, "processid"); + vm.Caption = cs.GetValue(csProperties, "caption")?.Trim(); + vm.Name = cs.GetValue(csProperties, "elementname")?.Trim(); + vm.Enabled = (EnabledEnum)cs.GetValue(csProperties, "enabledstate"); + vm.EnabledDefault = (EnabledDefaultEnum)cs.GetValue(csProperties, "enableddefault"); + vm.HealthState = (HealthStatusEnum)cs.GetValue(csProperties, "healthstate"); + vm.Status = cs.GetValue(csProperties, "status")?.Trim(); + vm.OnTime = cs.GetValue(csProperties, "ontimeinmilliseconds"); + vm.ReplicationMode = cs.GetValue(csProperties, "replicationmode"); + vm.ReplicationState = (ReplicationStateEnum)cs.GetValue(csProperties, "replicationstate"); + vm.ReplicationHealth = (ReplicationHealthEnum)cs.GetValue(csProperties, "replicationhealth"); + + if (cs.TryGetValue(csProperties, "installdate", out var installdate)) + { + vm.InstallDate = ManagementDateTimeConverter.ToDateTime(installdate?.ToString()); + } + + if (cs.TryGetValue(csProperties, "timeoflastconfigurationchange", out var timeoflastconfigurationchange)) + { + vm.TimeOfLastConfigurationChange = ManagementDateTimeConverter.ToDateTime(timeoflastconfigurationchange?.ToString()); + } + + if (cs.TryGetValue(csProperties, "timeoflaststatechange", out var timeoflaststatechange)) + { + vm.TimeOfLastStateChange = ManagementDateTimeConverter.ToDateTime(timeoflaststatechange?.ToString()); + } + + if (cs.TryGetValue(csProperties, "lastreplicationtime", out var lastreplicationtime)) + { + vm.LastReplicationTime = ManagementDateTimeConverter.ToDateTime(lastreplicationtime?.ToString()); + } + + var summaryinformation = cs.GetRelated("msvm_summaryinformation"); + using (summaryinformation) + { + foreach (ManagementObject si in summaryinformation.Cast()) + { + var siProperties = si.GetPropertyHashes(); + + vm.Notes = si.GetValue(siProperties, "Notes"); + vm.ConfigurationVersion = si.GetValue(siProperties, "Version"); + vm.IntegrationServicesVersionState = (IntegrationServicesVersionStateEnum)si.GetValue(siProperties, "IntegrationServicesVersionState"); + vm.GuestOperatingSystem = si.GetValue(siProperties, "GuestOperatingSystem"); + vm.NumberOfProcessors = si.GetValue(siProperties, "NumberOfProcessors"); + vm.ProcessorLoad = si.GetValue(siProperties, "ProcessorLoad"); + vm.MemoryAvailable = si.GetValue(siProperties, "MemoryAvailable"); + vm.MemoryUsage = si.GetValue(siProperties, "MemoryUsage"); + } + } + + var virtualSystemSettingData = cs.GetRelated("Msvm_VirtualSystemSettingData"); + using (virtualSystemSettingData) + { + var configs = new List(); + + foreach (ManagementObject vssd in virtualSystemSettingData.Cast()) + { + var vmc = new VirtualMaschineConfiguration(); + + var vssdProperties = vssd.GetPropertyHashes(); + + var vmcId = vssd.GetValue(vssdProperties, "ConfigurationID")?.Trim(); + + if (Guid.TryParse(vmcId, out var vmcGuid) is false) continue; + + vmc.Id = vmcGuid.ToString(); + + vmc.Type = vssd.GetValue(vssdProperties, "VirtualSystemType"); + vmc.Name = vssd.GetValue(vssdProperties, "ElementName"); + + if (vssd.TryGetValue(vssdProperties, "CreationTime", out var creationtime)) + { + vmc.CreationTime = ManagementDateTimeConverter.ToDateTime(creationtime?.ToString()); + } + + vmc.Generation = vssd.GetValue(vssdProperties, "VirtualSystemSubType"); + vmc.Architecture = vssd.GetValue(vssdProperties, "Architecture"); + vmc.AutomaticStartupAction = (AutomaticStartupActionEnum)vssd.GetValue(vssdProperties, "AutomaticStartupAction"); + + //if (vssd.TryGetValue(vssdProperties, "AutomaticStartupActionDelay", out var automaticstartupactiondelay)) + //{ + // vmc.CreationTime = ManagementDateTimeConverter.ToDateTime(automaticstartupactiondelay?.ToString()); + //} + + vmc.AutomaticShutdownAction = (AutomaticShutdownActionEnum)vssd.GetValue(vssdProperties, "AutomaticShutdownAction"); + vmc.AutomaticRecoveryAction = (AutomaticRecoveryActionEnum)vssd.GetValue(vssdProperties, "AutomaticRecoveryAction"); + vmc.AutomaticSnapshotsEnabled = vssd.GetValue(vssdProperties, "AutomaticSnapshotsEnabled"); + + vmc.BaseBoardSerialNumber = vssd.GetValue(vssdProperties, "BaseBoardSerialNumber"); + vmc.BIOSGUID = vssd.GetValue(vssdProperties, "BIOSGUID"); + vmc.BIOSSerialNumber = vssd.GetValue(vssdProperties, "BIOSSerialNumber"); + vmc.BootOrder = vssd.GetValue(vssdProperties, "BootOrder"); + vmc.ConfigurationDataRoot = vssd.GetValue(vssdProperties, "ConfigurationDataRoot"); + vmc.ConfigurationFile = vssd.GetValue(vssdProperties, "ConfigurationFile"); + vmc.GuestStateDataRoot = vssd.GetValue(vssdProperties, "GuestStateDataRoot"); + vmc.GuestStateFile = vssd.GetValue(vssdProperties, "GuestStateFile"); + vmc.SnapshotDataRoot = vssd.GetValue(vssdProperties, "SnapshotDataRoot"); + vmc.SuspendDataRoot = vssd.GetValue(vssdProperties, "SuspendDataRoot"); + vmc.SwapFileDataRoot = vssd.GetValue(vssdProperties, "SwapFileDataRoot"); + vmc.SecureBootEnabled = vssd.GetValue(vssdProperties, "SecureBootEnabled"); + vmc.IsAutomaticSnapshot = vssd.GetValue(vssdProperties, "IsAutomaticSnapshot"); + vmc.Notes = vssd.GetValue(vssdProperties, "Notes"); + vmc.ParentId = vssd.GetValue(vssdProperties, "Parent"); + + var storageallocationsettingdata = cs.GetRelated("Msvm_StorageAllocationSettingData"); + using (storageallocationsettingdata) + { + + } + + configs.Add(vmc); + } + + configs = configs.GroupBy(p => p.Id).Select(p => p.First()).ToList(); + if (configs.Any(p => p.ParentId is not null)) + { + foreach (var conf in configs.Where(p => p.ParentId is not null)) + { + using var parent = new ManagementObject(conf.ParentId); + parent.Get(); + + if (Guid.TryParse(parent["ConfigurationID"]?.ToString(), out var parentGuid) && configs.FirstOrDefault(p => p.Id == parentGuid.ToString()) is VirtualMaschineConfiguration parentConfig) + { + conf.ParentId = parentGuid.ToString(); + + parentConfig.Childs ??= new List(); + parentConfig.Childs.Add(conf); + } + else + { + conf.ParentId = null; + } + } + } + + vm.Configurations = configs.Where(p => p.ParentId is null).ToList(); + } + + vms.Add(vm); + } + } + + return vms; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Program.cs b/src/Agent/Insight.Agent/Program.cs new file mode 100644 index 0000000..005cb9e --- /dev/null +++ b/src/Agent/Insight.Agent/Program.cs @@ -0,0 +1,97 @@ +using Insight.Agent.Extensions; +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Agent.Network; +using Insight.Agent.Network.Handlers; +using Insight.Agent.Services; +using Insight.Domain.Constants; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Vaitr.Network; +using Vaitr.Network.Hosting; + +namespace Insight.Agent.Windows +{ + internal class Program + { + public static async Task Main(string[] args) + { + var builder = Host.CreateDefaultBuilder(args); + builder.UseWindowsService(); + builder.UseSystemd(); + + builder.ConfigureAppConfiguration(config => + { + config.Defaults(); + }); + + builder.ConfigureLogging(options => + { + options.ClearProviders(); + options.SetMinimumLevel(LogLevel.Trace); + + options.AddSimpleConsole(options => + { + options.IncludeScopes = true; + options.SingleLine = true; + options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff "; + }); + + options.AddFile($"{Configuration.AppDirectory?.FullName}/" + "logs/agent_{Date}.log", LogLevel.Trace, fileSizeLimitBytes: 104857600, retainedFileCountLimit: 10, outputTemplate: "{Timestamp:o} [{Level:u3}] {Message} {NewLine}{Exception}"); + }); + + builder.ConfigureServices((host, services) => + { + // SERVICES + services.AddHostedService(); + services.AddHostedService(); + + // SERVICES (WINDOWS) + if (OperatingSystem.IsWindows()) services.AddHostedService(); + + // AGENT NETWORKING + services.UseHostedClient(options => + { + options.Host = host.Configuration.GetValue(Appsettings.ServerHost) ?? throw new Exception($"{Appsettings.ServerHost} value not set (appsettings)"); + options.Port = host.Configuration.GetValue(Appsettings.ServerPort) ?? throw new Exception($"{Appsettings.ServerPort} value not set (appsettings)"); + options.Keepalive = 10000; + options.Timeout = 30000; + options.Encryption = Encryption.Tls12; + + options.UseSerializer, IAgentMessage>(); + }); + + services.AddSingleton, AuthenticationHandler>(); + services.AddSingleton, DriveHandler>(); + services.AddSingleton, InterfaceHandler>(); + services.AddSingleton, MainboardHandler>(); + services.AddSingleton, MemoryHandler>(); + services.AddSingleton, OperationSystemHandler>(); + services.AddSingleton, PrinterHandler>(); + services.AddSingleton, ProcessorHandler>(); + services.AddSingleton, ServiceHandler>(); + services.AddSingleton, SessionHandler>(); + services.AddSingleton, SoftwareHandler>(); + services.AddSingleton, StoragePoolHandler>(); + services.AddSingleton, SystemInfoHandler>(); + services.AddSingleton, UpdateHandler>(); + services.AddSingleton, UserHandler>(); + services.AddSingleton, VideocardHandler>(); + services.AddSingleton, VirtualMaschineHandler>(); + services.AddSingleton, ConsoleHandler>(); + + // GLOBAL DEPENDENCIES + services.AddTransient(provider => new HttpClient(new HttpClientHandler + { + ClientCertificateOptions = ClientCertificateOption.Manual, + ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true + })); + }); + + var host = builder.Build(); + await host.RunAsync().ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Properties/launchSettings.json b/src/Agent/Insight.Agent/Properties/launchSettings.json new file mode 100644 index 0000000..a22cba5 --- /dev/null +++ b/src/Agent/Insight.Agent/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "Development": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Production": { + "commandName": "Project" + } + } +} diff --git a/src/Agent/Insight.Agent/Services/CollectorService.cs b/src/Agent/Insight.Agent/Services/CollectorService.cs new file mode 100644 index 0000000..6b8d224 --- /dev/null +++ b/src/Agent/Insight.Agent/Services/CollectorService.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System.Runtime.Versioning; + +namespace Insight.Agent.Services +{ + [SupportedOSPlatform("linux")] + public partial class CollectorService + { + public ILogger Logger { get; } + + public CollectorService(ILogger? logger = null) + { + Logger = logger ?? NullLogger.Instance; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Services/Configurator.cs b/src/Agent/Insight.Agent/Services/Configurator.cs new file mode 100644 index 0000000..0778ac2 --- /dev/null +++ b/src/Agent/Insight.Agent/Services/Configurator.cs @@ -0,0 +1,97 @@ +using System.Text.Json; + +namespace Insight.Agent.Services +{ + public static class Configurator + { + public static async ValueTask ReadAsync(string file, CancellationToken cancellationToken = default) + where TConfig : class + { + var json = await File.ReadAllTextAsync(file, cancellationToken); + + if (JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, WriteIndented = true }) is not TConfig config) + { + throw new InvalidDataException($"Failed to deserialize ({file})"); + } + + return config; + } + public static async ValueTask ReadJsonAsync(string file, CancellationToken cancellationToken = default) + { + var json = await File.ReadAllTextAsync(file, cancellationToken); + + if (JsonDocument.Parse(json) is not JsonDocument doc) + { + throw new InvalidDataException($"Failed to deserialize ({file})"); + } + + return doc; + } + public static async ValueTask> ReadDictionaryAsync(string file, CancellationToken cancellationToken = default) + { + var json = await File.ReadAllTextAsync(file, cancellationToken); + + if (JsonSerializer.Deserialize>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, WriteIndented = true }) is not IDictionary config) + { + throw new InvalidDataException($"Failed to deserialize ({file})"); + } + + return config; + } + + public static async ValueTask WriteAsync(TConfig config, string file, CancellationToken cancellationToken) where TConfig : class + { + await WriteToFileAsync(config, file, cancellationToken); + } + public static async ValueTask WriteAsync(JsonDocument config, string file, CancellationToken cancellationToken) + { + await WriteToFileAsync(config, file, cancellationToken); + } + public static async ValueTask WriteAsync(IDictionary config, string file, CancellationToken cancellationToken) + { + await WriteToFileAsync(config, file, cancellationToken); + } + + public static async ValueTask AddOrUpdateAsync(KeyValuePair data, string file, CancellationToken cancellationToken) + { + var readData = await ReadDictionaryAsync(file, cancellationToken); + + var key = readData.Keys.FirstOrDefault(dic => string.Compare(dic, data.Key, StringComparison.OrdinalIgnoreCase) == 0); + + if (key is null) + { + readData.Add(data.Key, data.Value); + } + else + { + readData[key] = data.Value; + } + + await WriteToFileAsync(readData, file, cancellationToken); + } + public static async ValueTask RemoveAsync(string data, string file, CancellationToken cancellationToken) + { + var readData = await ReadDictionaryAsync(file, cancellationToken); + + var key = readData.Keys.FirstOrDefault(dic => string.Compare(dic, data, StringComparison.OrdinalIgnoreCase) == 0); + + if (key is null) + { + return; + } + else + { + readData.Remove(key); + } + + await WriteToFileAsync(readData, file, cancellationToken); + } + + private static async ValueTask WriteToFileAsync(TData data, string file, CancellationToken cancellationToken) + { + var json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }); + + await File.WriteAllTextAsync(file, json, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Services/EventService.cs b/src/Agent/Insight.Agent/Services/EventService.cs new file mode 100644 index 0000000..95a56e1 --- /dev/null +++ b/src/Agent/Insight.Agent/Services/EventService.cs @@ -0,0 +1,170 @@ +using Insight.Agent.Messages; +using Insight.Agent.Network; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Diagnostics.Eventing.Reader; +using System.Runtime.Versioning; +using System.Threading.Channels; +using Vaitr.Network; +using static Insight.Agent.Messages.Event; +using EventLevel = System.Diagnostics.Tracing.EventLevel; + +namespace Insight.Agent.Services +{ + [SupportedOSPlatform("windows")] + internal class EventService : BackgroundService + { + private readonly Channel _queue; + private readonly ISessionPool _pool; + private readonly ILogger _logger; + + public EventService(ISessionPool pool, ILogger logger) + { + _pool = pool; + _logger = logger; + + _queue = Channel.CreateBounded(new BoundedChannelOptions(1000) + { + SingleReader = true, + SingleWriter = true, + AllowSynchronousContinuations = false, + FullMode = BoundedChannelFullMode.DropOldest + }); + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + _logger.LogTrace("ExecuteAsync"); + + while (cancellationToken.IsCancellationRequested is false) + { + try + { + var tasks = new List + { + HandleQueueAsync(cancellationToken), + WatchAsync("Application", "*", cancellationToken), + WatchAsync("Security", "*", cancellationToken), + WatchAsync("System", "*", cancellationToken), + WatchAsync("Microsoft-Windows-PrintService/Admin", "*", cancellationToken), + WatchAsync("Microsoft-Windows-TaskScheduler/Operational", "*", cancellationToken), + WatchAsync("Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational", "*", cancellationToken), + WatchAsync("Microsoft-Windows-TerminalServices-LocalSessionManager/Operational", "*", cancellationToken), + WatchAsync("Microsoft-Windows-TerminalServices-RDPClient/Operational", "*", cancellationToken), + WatchAsync("Microsoft-Windows-SmbClient/Connectivity", "*", cancellationToken), + WatchAsync("Microsoft-Windows-SmbClient/Security", "*", cancellationToken), + WatchAsync("Microsoft-Windows-SMBServer/Security", "*", cancellationToken), + WatchAsync("Microsoft-Windows-StorageSpaces-Driver/Operational", "*", cancellationToken), + WatchAsync("Microsoft-Windows-Diagnostics-Performance/Operational", "*", cancellationToken) + }; + + await Task.WhenAll(tasks); + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + _logger.LogError("{ex}", ex); + } + } + } + + private async Task WatchAsync(string path, string query, CancellationToken cancellationToken) + { + var config = new EventLogConfiguration(path); + if (config is null) return; + + if (config.IsEnabled is false) + { + config.IsEnabled = true; + config.SaveChanges(); + } + + var watcher = new EventLogWatcher(new EventLogQuery(path, PathType.LogName, query) + { + TolerateQueryErrors = true, + Session = EventLogSession.GlobalSession + }); + + try + { + watcher.EventRecordWritten += new EventHandler(OnEvent); + watcher.Enabled = true; + + using var semaphore = new SemaphoreSlim(0, 1); + await semaphore.WaitAsync(cancellationToken); + } + catch (Exception) { } + finally + { + watcher.EventRecordWritten -= new EventHandler(OnEvent); + watcher.Enabled = false; + + watcher?.Dispose(); + } + } + + private async Task HandleQueueAsync(CancellationToken cancellationToken) + { + while (await _queue.Reader.WaitToReadAsync(cancellationToken)) + { + var session = _pool.FirstOrDefault(); + + if (session.Value is null) + { + await Task.Delay(10000, cancellationToken); + continue; + } + + if (_queue.Reader.TryRead(out var item) is false) + { + await Task.Delay(1000, cancellationToken); + continue; + } + + try + { + await session.Value.SendAsync(item, cancellationToken); + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + _logger.LogError("{ex}", ex); + await Task.Delay(10000, cancellationToken); + } + } + } + + private void OnEvent(object? sender, EventRecordWrittenEventArgs e) + { + if (e is null || e.EventRecord is null) return; + + try + { + var @event = new Event + { + Timestamp = e?.EventRecord?.TimeCreated, + EventId = e?.EventRecord?.Id, + Source = e?.EventRecord?.ProviderName, + Category = e?.EventRecord?.LogName, + Task = e?.EventRecord?.TaskDisplayName, + Message = e?.EventRecord?.FormatDescription(), + }; + + if (e?.EventRecord?.Level is not null) + { + @event.Status = (EventLevel)Convert.ToInt32(e?.EventRecord?.Level) switch + { + EventLevel.Informational => StatusType.Information, + EventLevel.Warning => StatusType.Warning, + EventLevel.Error => StatusType.Error, + EventLevel.Critical => StatusType.Critical, + _ => StatusType.Information, + }; + } + + _queue.Writer.WriteAsync(@event, default); + } + catch (Exception) { } // app crash + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Services/TrapService.cs b/src/Agent/Insight.Agent/Services/TrapService.cs new file mode 100644 index 0000000..b386e7f --- /dev/null +++ b/src/Agent/Insight.Agent/Services/TrapService.cs @@ -0,0 +1,200 @@ +using Insight.Agent.Messages; +using Insight.Agent.Network; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Snmp; +using System.Net; +using System.Net.Sockets; +using System.Threading.Channels; +using Vaitr.Network; + +namespace Insight.Agent.Services +{ + public class TrapService : BackgroundService + { + private static IPEndPoint EndpointCache { get; } = new(IPAddress.Any, 0); + private readonly Channel _queue; + + + private readonly int _port; + private readonly ISessionPool _pool; + private readonly ILogger _logger; + + public TrapService(ISessionPool pool, IConfiguration configuration, ILogger logger) + { + _port = configuration.GetValue(Appsettings.TrapPort) ?? throw new Exception($"{Appsettings.TrapPort} value not set (appsettings)"); + _pool = pool; + _logger = logger; + + _queue = Channel.CreateBounded(new BoundedChannelOptions(100) + { + SingleReader = false, + SingleWriter = true, + AllowSynchronousContinuations = false, + FullMode = BoundedChannelFullMode.DropOldest + }); + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + _logger.LogTrace("ExecuteAsync"); + + while (cancellationToken.IsCancellationRequested is false) + { + try + { + var tasks = new List + { + HandleQueueAsync(cancellationToken), + ListenAsync(cancellationToken) + }; + + await Task.WhenAll(tasks); + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + _logger.LogError("{ex}", ex); + } + } + } + + private async Task HandleQueueAsync(CancellationToken cancellationToken) + { + while (await _queue.Reader.WaitToReadAsync(cancellationToken)) + { + var session = _pool.FirstOrDefault(); + + if (session.Value is null) + { + await Task.Delay(10000, cancellationToken); + continue; + } + + if (_queue.Reader.TryRead(out var item) is false) + { + await Task.Delay(1000, cancellationToken); + continue; + } + + try + { + await session.Value.SendAsync(item, cancellationToken); + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + _logger.LogError("{ex}", ex); + await Task.Delay(10000, cancellationToken); + } + } + } + + private async Task ListenAsync(CancellationToken cancellationToken) + { + using var udpSocket = new Socket(SocketType.Dgram, ProtocolType.Udp); + + cancellationToken.Register(udpSocket.Dispose); + + udpSocket.Bind(new IPEndPoint(IPAddress.Any, _port)); + + var buffer = GC.AllocateArray(length: 65527, pinned: true); + var bufferMem = buffer.AsMemory(); + + while (cancellationToken.IsCancellationRequested is false) + { + try + { + var result = await udpSocket.ReceiveFromAsync(bufferMem, SocketFlags.None, EndpointCache, cancellationToken); + + if (result.ReceivedBytes == 0) continue; + + var actualBytes = bufferMem[..result.ReceivedBytes].ToArray(); + + var ep = (IPEndPoint)result.RemoteEndPoint; + + await ProcessAsync(ep, actualBytes, cancellationToken); + } + catch (SocketException) { continue; } + catch (IOException) { continue; } + catch (Exception) { continue; } + } + } + + private async ValueTask ProcessAsync(IPEndPoint endpoint, byte[] buffer, CancellationToken cancellationToken) + { + var trap = new Trap + { + Timestamp = DateTime.Now, + Endpoint = endpoint.ToString(), + }; + + try + { + var protocol = (SnmpVersion)SnmpPacket.GetProtocolVersion(buffer, buffer.Length); + + trap.Version = protocol.ToString(); + trap.Hostname = Dns.GetHostEntry(endpoint.Address)?.HostName; + + if (protocol == SnmpVersion.Ver1) + { + var packet = new SnmpV1TrapPacket(); + packet.decode(buffer, buffer.Length); + + trap.Community = packet?.Community?.ToString(); + + if (packet?.Pdu?.VbList is not null) + { + trap.Data = ConvertVbs(packet.Pdu.VbList); + } + } + + if (protocol == SnmpVersion.Ver2) + { + var packet = new SnmpV2Packet(); + packet.decode(buffer, buffer.Length); + + trap.Community = packet?.Community?.ToString(); + + if (packet?.Pdu?.VbList is not null) + { + trap.Data = ConvertVbs(packet.Pdu.VbList); + } + } + + if (protocol == SnmpVersion.Ver3) + { + var packet = new SnmpV3Packet(); + } + } + catch (Exception ex) + { + _logger.LogError("{ex}", ex); + } + + await _queue.Writer.WriteAsync(trap, cancellationToken); + } + + private List>? ConvertVbs(VbCollection vbs) + { + var data = new List>(); + + try + { + foreach (var item in vbs) + { + if (item.Oid is null) continue; + + data.Add(new KeyValuePair(item.Oid.ToString(), item?.Value?.ToString())); + } + } + catch (Exception ex) + { + _logger.LogError("{ex}", ex); + } + + return data; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Services/UpdateService.cs b/src/Agent/Insight.Agent/Services/UpdateService.cs new file mode 100644 index 0000000..6869d58 --- /dev/null +++ b/src/Agent/Insight.Agent/Services/UpdateService.cs @@ -0,0 +1,360 @@ +using Insight.Agent.Constants; +using Insight.Domain.Models; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Diagnostics; +using System.IO.Compression; +using System.Net.Http.Json; +using System.Runtime.Versioning; +using System.ServiceProcess; +using System.Text.Json; + +namespace Insight.Agent.Services +{ + internal class UpdateService : BackgroundService + { + private readonly Uri _uri; + + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public UpdateService(HttpClient httpClient, IConfiguration configuration, ILogger logger) + { + _httpClient = httpClient; + _uri = configuration.GetValue("api") ?? throw new Exception($"api value not set (appsettings)"); + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + _logger.LogTrace("ExecuteAsync"); + + while (cancellationToken.IsCancellationRequested is false) + { + try + { + UpdateResult? result = null; + + if (OperatingSystem.IsWindows()) result = await WindowsUpdateAsync(cancellationToken); + if (OperatingSystem.IsLinux()) result = await LinuxUpdateAsync(cancellationToken); + + _logger.LogInformation("Update Result: {result}", result?.Success); + if (result?.UpdateErrors is not null) + { + _logger.LogError("Update Errors: {errors}", string.Concat(result?.UpdateErrors)); + } + } + catch (OperationCanceledException) { } + catch (Exception ex) // may inform via client / api about errors + { + _logger.LogError("{ex}", ex.Message); + } + finally + { + await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken); + } + } + } + + [SupportedOSPlatform("windows")] + private async ValueTask WindowsUpdateAsync(CancellationToken cancellationToken) + { + return await Windows.Service.UpdateAsync( + _httpClient, + Deploy.GetUpdateHref(_uri, Deploy.Updater.Name), + Deploy.GetAppExecutable(Deploy.Updater.Name), + Deploy.Updater.ServiceName, + cancellationToken); + } + + [SupportedOSPlatform("linux")] + private ValueTask LinuxUpdateAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + [SupportedOSPlatform("windows")] + private static class Windows + { + public static class Service + { + public static bool ServiceExistence(string serviceName) + { + try + { + if (ServiceController.GetServices().Any(s => s.ServiceName.Equals(serviceName, StringComparison.InvariantCultureIgnoreCase))) return true; + return false; + } + catch (Exception) { } + + return false; + } + + public static bool SetServiceState(string app, ServiceControllerStatus status, TimeSpan timeout) + { + try + { + using var sc = ServiceController.GetServices().FirstOrDefault(s => s.ServiceName.Equals(app, StringComparison.InvariantCultureIgnoreCase)); + if (sc is null) return false; + + if (sc.Status != status) + { + switch (status) + { + case ServiceControllerStatus.Running: + sc.Start(); + break; + + case ServiceControllerStatus.Stopped: + sc.Stop(); + break; + } + + sc.WaitForStatus(status, timeout); + } + + return true; + } + catch (Exception) { } + + return false; + } + + public static async ValueTask UpdateAsync(HttpClient httpClient, Uri api, FileInfo bin, string serviceName, CancellationToken cancellationToken) + { + var result = new UpdateResult + { + Api = api?.ToString(), + SourceDirectory = bin.Directory?.FullName, + App = bin.Name, + ServiceName = serviceName + }; + + try + { + // check if service exists + if (ServiceExistence(serviceName) is false) + { + result.UpdateErrors.Add("service not found"); + return result; + } + + // get service update details + var response = await httpClient.GetFromJsonAsync(api, cancellationToken); + if (response is null) + { + result.ApiErrors.Add("not available / response null"); + return result; + } + + result.ApiAvailable = true; + + // check if local binary exists + if (bin is null) + { + result.UpdateErrors.Add("source binary not found"); + return result; + } + + // get local file binary version + if (FileVersionInfo.GetVersionInfo(bin.FullName).FileVersion is not string binVersionString) + { + result.UpdateErrors.Add("source binary fileversion not valid"); + return result; + } + + // compare local against update version, skip lower or equal update version + var actualVersion = Version.Parse(binVersionString); + if (actualVersion >= response.Version) + { + result.Success = true; + return result; + } + else + { + result.UpdateAvailable = true; + } + + // get update file (bytes) to memory + using var update = await httpClient.GetAsync(response.Uri, cancellationToken); + if (update is null) + { + result.ApiErrors.Add("update source not available"); + return result; + } + + // stop service + if (SetServiceState(serviceName, ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(10)) is false) + { + result.UpdateErrors.Add("service control failed / failed to stop service"); + return result; + } + + // read update archive to temp (overwrite) + var temp = Directory.CreateTempSubdirectory(); + var updateFile = new FileInfo($@"{temp.FullName}/{bin.Name}.zip"); + + await File.WriteAllBytesAsync(updateFile.FullName, await update.Content.ReadAsByteArrayAsync(cancellationToken), cancellationToken); + + // extract update archive from temp to app dir (overwrite) + ZipFile.ExtractToDirectory(updateFile.FullName, bin.Directory?.FullName, true); + + // delete temp folder + if (temp.Exists) temp.Delete(true); + + // start updateds service + if (SetServiceState(serviceName, ServiceControllerStatus.Running, TimeSpan.FromSeconds(10)) is false) + { + result.UpdateErrors.Add("service control failed / failed to start service"); + return result; + } + + result.Success = true; + } + catch (Exception ex) + { + result.UpdateErrors.Add(ex.Message); + } + + return result; + } + } + + public static class Process + { + public static bool IsRunning(FileInfo bin) + { + if (bin.Exists is false) return false; + + var matched = System.Diagnostics.Process.GetProcessesByName(bin.FullName); + + if (matched is null || matched.Any() is false) return false; + + if (matched.Any(p => + p.MainModule is not null && + p.MainModule.FileName is not null && + p.MainModule.FileName.Equals(bin.FullName, + StringComparison.InvariantCultureIgnoreCase))) return true; + + return false; + } + + public static bool Start(FileInfo binary) + { + try + { + if (IsRunning(binary) is false) return false; + + using var process = System.Diagnostics.Process.Start(binary.FullName); + return true; + } + catch (Exception) { } + + return false; + } + + public static bool Stop(FileInfo bin, TimeSpan timeout) + { + try + { + if (IsRunning(bin) is false) return false; + + var matched = System.Diagnostics.Process.GetProcessesByName(bin.FullName); + + if (matched is null || matched.Any() is false) return true; + + foreach (var procsInfo in matched.Where(p => + p.MainModule is not null && + p.MainModule.FileName is not null && + p.MainModule.FileName.Equals(bin.FullName, StringComparison.InvariantCultureIgnoreCase))) + { + if (procsInfo.CloseMainWindow()) procsInfo.WaitForExit((int)timeout.TotalMilliseconds); + if (procsInfo.HasExited is false) procsInfo.Kill(true); + } + + return true; + } + catch (Exception) { } + + return false; + } + + public static async ValueTask UpdateAsync(HttpClient httpClient, Uri api, FileInfo bin, CancellationToken cancellationToken) + { + if (IsRunning(bin) is false) return false; + + var response = await httpClient.GetFromJsonAsync(api.AbsoluteUri, new JsonSerializerOptions + { + IncludeFields = true + }, cancellationToken); + + if (response is null) return false; + + Version actualVersion; + + if (FileVersionInfo.GetVersionInfo(bin.FullName).FileVersion is not string binVersionString) return false; + + try + { + actualVersion = Version.Parse(binVersionString); + if (actualVersion >= response.Version) return false; + + using var update = await httpClient.GetAsync(response.Uri, cancellationToken); + if (update is null) return false; + + Stop(bin, TimeSpan.FromSeconds(60)); + + // read update archive to temp (overwrite) + var temp = Directory.CreateTempSubdirectory(); + var updateFile = new FileInfo($@"{temp.FullName}/{bin.Name}.zip"); + + await File.WriteAllBytesAsync(updateFile.FullName, await update.Content.ReadAsByteArrayAsync(cancellationToken), cancellationToken); + + // extract update archive from temp to app dir (overwrite) + ZipFile.ExtractToDirectory(updateFile.FullName, bin.Directory?.FullName, true); + + // delete temp folder + if (temp.Exists) temp.Delete(true); + + // rewrite with options to start user session process + //Start(app, directory, TimeSpan.FromSeconds(60)); + + return true; + } + catch (Exception) { } + + return false; + } + + public static bool Delete(FileInfo bin, TimeSpan timeout) + { + try + { + Stop(bin, timeout); + bin.Delete(); + + return true; + } + catch (Exception) { } + + return false; + } + } + } + + public class UpdateResult + { + public string? Api { get; set; } + public string? SourceDirectory { get; set; } + public string? App { get; set; } + public string? ServiceName { get; set; } + + public bool ApiAvailable { get; set; } = false; + public bool UpdateAvailable { get; set; } = false; + public bool Success { get; set; } = false; + public List ApiErrors { get; } = new(); + public List UpdateErrors { get; } = new(); + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Services/_Collector/_Os.cs b/src/Agent/Insight.Agent/Services/_Collector/_Os.cs new file mode 100644 index 0000000..7b3eb0e --- /dev/null +++ b/src/Agent/Insight.Agent/Services/_Collector/_Os.cs @@ -0,0 +1,106 @@ +using Insight.Agent.Extensions; +using Insight.Agent.Messages; +using Microsoft.Extensions.Logging; +using System.Text.RegularExpressions; + +namespace Insight.Agent.Services +{ + public partial class CollectorService + { + public OperationSystem? GetOperatingSystem() + { + Logger.LogTrace("GetOperatingSystem"); + + var os = new OperationSystem(); + + // get uptime + var output = string.Empty; + + // read file + using var stream = File.OpenText(@"/proc/uptime"); + output = stream.ReadToEnd(); + + // clean output + var clean = Regex + .Replace(output + .Trim() + .Replace("\t", " "), @"[ ]{2,}", " "); + + var elements = clean.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries); + + // assign values + //os.Uptime = DateTime.Now - TimeSpan.FromSeconds(double.Parse(elements.ElementAt(0))); + + // request data with process + output = "hostnamectl".Bash(); + + // linebreak list conversion + var lines = new List(output + .Split(new string[] { "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries)) + .Select(l => + { + return Regex + .Replace(l + .Trim() + .Replace("\t", " "), @"[ ]{2,}", " "); + }) + .ToList(); + + // assign values + os.Virtual = lines + .Any(l => l + .StartsWith("Virtualization:")) && !string.IsNullOrEmpty(lines + .Where(l => l + .StartsWith("Virtualization:")) + .First() + .Split("Virtualization:")[1] + .Trim()); + + // OS + // request data with process + output = "hostnamectl".Bash(); + + // linebreak list conversion + lines = new List(output + .Split(new string[] { "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries)) + .Select(l => + { + return Regex + .Replace(l + .Trim() + .Replace("\t", " "), @"[ ]{2,}", " "); + }) + .ToList(); + + // assign values + os.Name = lines + .Any(l => l + .StartsWith("Operating System:")) ? lines + .Where(l => l + .StartsWith("Operating System:")) + .First() + .Split("Operating System:")[1] + .Trim() : string.Empty; + + os.Version = lines + .Any(l => l + .StartsWith("Kernel:")) ? lines + .Where(l => l + .StartsWith("Kernel:")) + .First() + .Split("Kernel:")[1] + .Trim() : string.Empty; + + var architecture = lines + .Any(l => l + .StartsWith("Architecture:")) ? lines + .Where(l => l + .StartsWith("Architecture:")) + .First() + .Split("Architecture:")[1] + .Trim() : string.Empty; + + return os; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/Services/_Collector/_Session.cs b/src/Agent/Insight.Agent/Services/_Collector/_Session.cs new file mode 100644 index 0000000..3b34213 --- /dev/null +++ b/src/Agent/Insight.Agent/Services/_Collector/_Session.cs @@ -0,0 +1,50 @@ +using Insight.Agent.Extensions; +using Insight.Agent.Messages; +using Microsoft.Extensions.Logging; +using System.Text.RegularExpressions; + +namespace Insight.Agent.Services +{ + public partial class CollectorService + { + public List? GetSessions() + { + Logger.LogTrace("GetSessions"); + + var sessions = new List(); + + // request data with process + var output = "w".Bash(); + + // linebreak list conversion + var lines = output.Split(new string[] { "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries).ToList(); + + lines = lines.Select(l => + { + return Regex.Replace(l.Trim().Replace("\t", " "), @"[ ]{2,}", " "); + }).ToList(); + + // cleaning + lines.RemoveRange(0, 2); + + // process list elements + foreach (var l in lines) + { + // split into elements + var elements = l.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries); + + // add user, assign values + sessions.Add(new Session + { + User = elements.ElementAt(0), + //Id = elements.ElementAt(1), + Remote = elements.ElementAt(2), + //Login = elements.ElementAt(3), + //Idle = elements.ElementAt(4) + }); + } + + return sessions; + } + } +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/appsettings.Development.json b/src/Agent/Insight.Agent/appsettings.Development.json new file mode 100644 index 0000000..ed0bde7 --- /dev/null +++ b/src/Agent/Insight.Agent/appsettings.Development.json @@ -0,0 +1,6 @@ +{ + "api": "http://insight.local/api", + "trap.port": 162, + "server.host": "insight.local", + "server.port": 3002 +} \ No newline at end of file diff --git a/src/Agent/Insight.Agent/appsettings.json b/src/Agent/Insight.Agent/appsettings.json new file mode 100644 index 0000000..31baeff --- /dev/null +++ b/src/Agent/Insight.Agent/appsettings.json @@ -0,0 +1,6 @@ +{ + "api": "https://insight.webmatic.de/api", + "trap.port": 162, + "server.host": "insight.webmatic.de", + "server.port": 3002 +} \ No newline at end of file diff --git a/src/Api/Insight.Api/.config/dotnet-tools.json b/src/Api/Insight.Api/.config/dotnet-tools.json new file mode 100644 index 0000000..34c3e19 --- /dev/null +++ b/src/Api/Insight.Api/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "7.0.5", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/src/Api/Insight.Api/Constants/Locations.cs b/src/Api/Insight.Api/Constants/Locations.cs new file mode 100644 index 0000000..8db6a8f --- /dev/null +++ b/src/Api/Insight.Api/Constants/Locations.cs @@ -0,0 +1,9 @@ +using Insight.Domain.Constants; + +namespace Insight.Api +{ + public static class Locations + { + public static DirectoryInfo UpdatesPath { get; } = new DirectoryInfo($"{Configuration.AppDirectory?.FullName}/files/updates"); + } +} \ No newline at end of file diff --git a/src/Api/Insight.Api/Controllers/AccountController.cs b/src/Api/Insight.Api/Controllers/AccountController.cs new file mode 100644 index 0000000..21756f3 --- /dev/null +++ b/src/Api/Insight.Api/Controllers/AccountController.cs @@ -0,0 +1,76 @@ +using Insight.Api.Models; +using Insight.Infrastructure.Models; +using Insight.Infrastructure.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Insight.Api.Controllers +{ + [ApiController, Route("api/accounts")] + public class AccountController : ControllerBase + { + private readonly IdentityService _identityService; + private readonly AccountService _accountService; + private readonly ILogger _logger; + + public AccountController(IdentityService identityService, AccountService accountService, ILogger logger) + { + _identityService = identityService; + _accountService = accountService; + _logger = logger; + } + + [HttpGet, Authorize] + public async Task Get([FromQuery] PagedDataRequest request, CancellationToken cancellationToken) + { + try + { + var result = await _accountService.GetAsync( + offset: request.Offset, + limit: request.Limit, + request: Request, + response: Response, + cancellationToken: cancellationToken).ConfigureAwait(false); + + return Ok(result); + } + catch (UnauthorizedAccessException ex) + { + return Unauthorized(ex.ToString()); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpPost("register"), Authorize] + public async Task Register([FromBody] RegistrationModel model) + { + if (string.IsNullOrWhiteSpace(model.Email)) return BadRequest("Email Required"); + if (string.IsNullOrWhiteSpace(model.Password)) return BadRequest("Password Required"); + if (string.IsNullOrWhiteSpace(model.ConfirmPassword)) return BadRequest("Password Confirmation Required"); + + if (model.Password != model.ConfirmPassword) return BadRequest("Passwords do not match"); + + try + { + var result = await _identityService.CreateUserAsync(model.Email, model.Password).ConfigureAwait(false); + + if (result.Succeeded is false) return BadRequest(result.Errors); + } + catch (UnauthorizedAccessException ex) + { + return Unauthorized(ex.Message); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + + //await _userManager.AddToRoleAsync(user, "Visitor"); + + return Ok(model.Email); + } + } +} \ No newline at end of file diff --git a/src/Api/Insight.Api/Controllers/AgentController.cs b/src/Api/Insight.Api/Controllers/AgentController.cs new file mode 100644 index 0000000..cc0f6b1 --- /dev/null +++ b/src/Api/Insight.Api/Controllers/AgentController.cs @@ -0,0 +1,44 @@ +using Insight.Infrastructure.Models; +using Insight.Infrastructure.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Insight.Api.Controllers +{ + [ApiController, Route("api/agents")] + public class AgentController : ControllerBase + { + private readonly AgentService _agentService; + private readonly ILogger _logger; + + public AgentController(AgentService agentService, ILogger logger) + { + _agentService = agentService; + _logger = logger; + } + + [HttpGet, Authorize] + public async Task Get([FromQuery] PagedDataRequest request, CancellationToken cancellationToken) + { + try + { + var result = await _agentService.GetAsync( + offset: request.Offset, + limit: request.Limit, + request: Request, + response: Response, + cancellationToken: cancellationToken).ConfigureAwait(false); + + return Ok(result); + } + catch (UnauthorizedAccessException ex) + { + return Unauthorized(ex.ToString()); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + } +} \ No newline at end of file diff --git a/src/Api/Insight.Api/Controllers/CustomerController.cs b/src/Api/Insight.Api/Controllers/CustomerController.cs new file mode 100644 index 0000000..6733f81 --- /dev/null +++ b/src/Api/Insight.Api/Controllers/CustomerController.cs @@ -0,0 +1,44 @@ +using Insight.Infrastructure.Models; +using Insight.Infrastructure.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Insight.Api.Controllers +{ + [ApiController, Route("api/customers")] + public class CustomerController : ControllerBase + { + private readonly CustomerService _customerService; + private readonly ILogger _logger; + + public CustomerController(CustomerService customerService, ILogger logger) + { + _customerService = customerService; + _logger = logger; + } + + [HttpGet, Authorize] + public async Task Get([FromQuery] PagedDataRequest request, CancellationToken cancellationToken) + { + try + { + var result = await _customerService.GetAsync( + offset: request.Offset, + limit: request.Limit, + request: Request, + response: Response, + cancellationToken: cancellationToken).ConfigureAwait(false); + + return Ok(result); + } + catch (UnauthorizedAccessException ex) + { + return Unauthorized(ex.ToString()); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + } +} \ No newline at end of file diff --git a/src/Api/Insight.Api/Controllers/HostController.cs b/src/Api/Insight.Api/Controllers/HostController.cs new file mode 100644 index 0000000..bd42dac --- /dev/null +++ b/src/Api/Insight.Api/Controllers/HostController.cs @@ -0,0 +1,44 @@ +using Insight.Infrastructure.Models; +using Insight.Infrastructure.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Insight.Api.Controllers +{ + [ApiController, Route("api/hosts")] + public class HostController : ControllerBase + { + private readonly HostService _hostService; + private readonly ILogger _logger; + + public HostController(HostService hostService, ILogger logger) + { + _hostService = hostService; + _logger = logger; + } + + [HttpGet, Authorize] + public async Task Get([FromQuery] PagedDataRequest request, CancellationToken cancellationToken) + { + try + { + var result = await _hostService.GetAsync( + offset: request.Offset, + limit: request.Limit, + request: Request, + response: Response, + cancellationToken: cancellationToken).ConfigureAwait(false); + + return Ok(result); + } + catch (UnauthorizedAccessException ex) + { + return Unauthorized(ex.ToString()); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + } +} \ No newline at end of file diff --git a/src/Api/Insight.Api/Controllers/InventoryController.cs b/src/Api/Insight.Api/Controllers/InventoryController.cs new file mode 100644 index 0000000..2a47dff --- /dev/null +++ b/src/Api/Insight.Api/Controllers/InventoryController.cs @@ -0,0 +1,73 @@ +using Insight.Infrastructure.Entities; +using Insight.Infrastructure.Models; +using Insight.Infrastructure.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; +using MongoDB.Driver; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace Insight.Api.Controllers +{ + [ApiController, Route("api/inventory")] + public class InventoryController : ControllerBase + { + private readonly InventoryService _inventoryService; + private readonly ILogger _logger; + + public InventoryController(InventoryService inventoryService, ILogger logger) + { + _inventoryService = inventoryService; + _logger = logger; + } + + [HttpGet, Authorize] + public async Task Get([FromQuery] HostApplicationEntity request, [FromQuery] PagedDataRequest meta, CancellationToken cancellationToken) + { + try + { + var filter = Builders.Filter.Empty; + + if (request.Id is not null) + filter &= Builders.Filter.Eq(p => p.Id, request.Id); + + if (request.Host is not null) + filter &= Builders.Filter.Eq(p => p.Host, request.Host); + + if (request.Name is not null) + filter &= Builders.Filter.Regex(p => p.Name, new BsonRegularExpression(new Regex(request.Name, RegexOptions.IgnoreCase))); + + var result = await _inventoryService.GetAsync( + filter: filter, + offset: meta.Offset, + limit: meta.Limit, + request: Request, + response: Response, + cancellationToken: cancellationToken).ConfigureAwait(false); + + return Ok(result); + } + catch (UnauthorizedAccessException ex) + { + return Unauthorized(ex.ToString()); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + public class ApplicationRequest + { + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("host")] + public string? Host { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Api/Insight.Api/Controllers/SetupController.cs b/src/Api/Insight.Api/Controllers/SetupController.cs new file mode 100644 index 0000000..913e294 --- /dev/null +++ b/src/Api/Insight.Api/Controllers/SetupController.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Insight.Server.Controllers +{ + [ApiController, Route("api/setup")] + public class SetupController : ControllerBase + { + private readonly ILogger _logger; + + public SetupController(ILogger logger) + { + _logger = logger; + } + + [HttpGet("windows")] + public async Task GetAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("[{method}] {route} => {ep}", Request.Method, Request.HttpContext.Request.Path, Request.HttpContext.Connection.RemoteIpAddress); + + var files = new DirectoryInfo($"{Domain.Constants.Configuration.AppDirectory?.FullName}/files/setup").GetFiles(); + + if (files.Length == 0) + { + return NotFound(); + } + + return File(await System.IO.File.ReadAllBytesAsync(files.OrderBy(p => p.LastWriteTime).First().FullName, cancellationToken), "application/zip", "setup-win64.zip"); + } + } +} \ No newline at end of file diff --git a/src/Api/Insight.Api/Controllers/TokenController.cs b/src/Api/Insight.Api/Controllers/TokenController.cs new file mode 100644 index 0000000..372a066 --- /dev/null +++ b/src/Api/Insight.Api/Controllers/TokenController.cs @@ -0,0 +1,85 @@ +using Insight.Domain.Models; +using Insight.Infrastructure.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Insight.Api.Controllers +{ + [ApiController, Route("api/token", Order = 0)] + public class TokenController : ControllerBase + { + private readonly TokenService _tokenService; + + public TokenController(TokenService tokenService) + { + _tokenService = tokenService; + } + + /// + /// Access Token Request + /// + [HttpPost, AllowAnonymous] + public async Task Authentication([FromBody] TokenRequest request) + { + try + { + var result = await _tokenService.GetAsync(request.Username, request.Password, request.Code, HttpContext.Connection.RemoteIpAddress).ConfigureAwait(false); + return Ok(result); + } + catch (UnauthorizedAccessException ex) + { + return Unauthorized(ex.Message); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + /// + /// Refresh Token Request + /// + [HttpPost("refresh"), AllowAnonymous] + public async Task Refresh([FromBody] TokenRefreshRequest request) + { + if (string.IsNullOrWhiteSpace(request.Token)) return BadRequest("Refresh Token Required"); + + try + { + var result = await _tokenService.RefreshAsync(request.Token, HttpContext.Connection.RemoteIpAddress).ConfigureAwait(false); + return Ok(result); + } + catch (UnauthorizedAccessException ex) + { + return Unauthorized(ex.Message); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + /// + /// Revoke Token Request + /// + [HttpPost("revoke"), AllowAnonymous] + public async Task Revoke([FromBody] TokenRevokeRequest request) + { + if (string.IsNullOrWhiteSpace(request.Token)) return BadRequest("Refresh Token Required"); + + try + { + await _tokenService.RevokeAsync(request.Token, request.Reason ?? "revoked by user", HttpContext.Connection.RemoteIpAddress).ConfigureAwait(false); + return Ok(); + } + catch (UnauthorizedAccessException ex) + { + return Unauthorized(ex.Message); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + } +} \ No newline at end of file diff --git a/src/Api/Insight.Api/Controllers/UpdateController.cs b/src/Api/Insight.Api/Controllers/UpdateController.cs new file mode 100644 index 0000000..6ff09df --- /dev/null +++ b/src/Api/Insight.Api/Controllers/UpdateController.cs @@ -0,0 +1,93 @@ +using Insight.Api; +using Insight.Domain.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Insight.Server.Controllers +{ + [ApiController, Route("api/update")] + public class UpdateController : ControllerBase + { + private readonly ILogger _logger; + + public UpdateController(ILogger logger) + { + _logger = logger; + } + + [HttpGet("updater/windows")] + public IActionResult UpdaterWindows() + { + _logger.LogInformation("[{method}] {route} => {ep}", Request.Method, Request.HttpContext.Request.Path, Request.HttpContext.Connection.RemoteIpAddress); + + var updateDir = new DirectoryInfo($"{Locations.UpdatesPath}/updater/windows"); + + if (updateDir.Exists is false) + { + return NotFound(); + } + + var updateLock = new FileInfo($"{updateDir.FullName}/.lock"); + + if (updateLock.Exists) + { + return NotFound("locked"); + } + + var versions = updateDir.GetFiles("*.zip", SearchOption.TopDirectoryOnly); + + if (versions is null || versions.Any() is false) return NotFound(); + + var latest = versions.OrderBy(x => x.Name).FirstOrDefault(); + + if (latest is null) return NotFound(); + + var relPath = $"{Path.GetRelativePath($"{Domain.Constants.Configuration.AppDirectory?.FullName}", latest.FullName)}"; + + if (Version.TryParse(Path.GetFileNameWithoutExtension(latest.Name), out var fileversion) is false) return NotFound(); + + return Ok(new UpdateResponse + { + Version = fileversion, + Uri = new Uri($"{Request.Scheme}://{Request.Host}/api/{relPath}") + }); + } + + [HttpGet("agent/windows")] + public IActionResult AgentWindows() + { + _logger.LogInformation("[{method}] {route} => {ep}", Request.Method, Request.HttpContext.Request.Path, Request.HttpContext.Connection.RemoteIpAddress); + + var updateDir = new DirectoryInfo($"{Locations.UpdatesPath}/agent/windows"); + + if (updateDir.Exists is false) + { + return NotFound(); + } + + var updateLock = new FileInfo($"{updateDir.FullName}/.lock"); + + if (updateLock.Exists) + { + return NotFound("locked"); + } + + var versions = updateDir.GetFiles("*.zip", SearchOption.TopDirectoryOnly); + + if (versions is null || versions.Any() is false) return NotFound(); + + var latest = versions.OrderBy(x => x.Name).FirstOrDefault(); + + if (latest is null) return NotFound(); + + var relPath = $"{Path.GetRelativePath($"{Domain.Constants.Configuration.AppDirectory?.FullName}", latest.FullName)}"; + + if (Version.TryParse(Path.GetFileNameWithoutExtension(latest.Name), out var fileversion) is false) return NotFound(); + + return Ok(new UpdateResponse + { + Version = fileversion, + Uri = new Uri($"{Request.Scheme}://{Request.Host}/api/{relPath}") + }); + } + } +} \ No newline at end of file diff --git a/src/Api/Insight.Api/Extensions/ServiceExtensions.cs b/src/Api/Insight.Api/Extensions/ServiceExtensions.cs new file mode 100644 index 0000000..46b91a7 --- /dev/null +++ b/src/Api/Insight.Api/Extensions/ServiceExtensions.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.OpenApi.Models; +using System.Reflection; + +namespace Insight.Api.Hosting +{ + public static class ServiceExtensions + { + internal static IServiceCollection AddSwaggerServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "Insight API", + Version = "v1" + }); + + options.AddSecurityDefinition(name: "Bearer", securityScheme: new OpenApiSecurityScheme + { + Name = "Authorization", + Description = "Enter the Bearer Authorization string as following: `Bearer Generated-JWT-Token`", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer", + BearerFormat = "JWT", + + Reference = new OpenApiReference + { + Id = JwtBearerDefaults.AuthenticationScheme, + Type = ReferenceType.SecurityScheme + } + }); + + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Reference = new OpenApiReference + { + Id = "Bearer", + Type = ReferenceType.SecurityScheme + } + }, + new List() + } + }); + + var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); + }); + + return services; + } + } +} \ No newline at end of file diff --git a/src/Api/Insight.Api/Insight.Api.csproj b/src/Api/Insight.Api/Insight.Api.csproj new file mode 100644 index 0000000..95b64f1 --- /dev/null +++ b/src/Api/Insight.Api/Insight.Api.csproj @@ -0,0 +1,50 @@ + + + + net7.0 + Insight + api + 2023.8.23.1 + Insight.Api + enable + enable + 4ae1d3bf-869e-4963-8a19-35634507d3b3 + + false + false + + + True + $(NoWarn);1591 + + + + none + + + + none + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Api/Insight.Api/Models/RegistrationModel.cs b/src/Api/Insight.Api/Models/RegistrationModel.cs new file mode 100644 index 0000000..206b2dc --- /dev/null +++ b/src/Api/Insight.Api/Models/RegistrationModel.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace Insight.Api.Models +{ + public class RegistrationModel + { + public string? FirstName { get; set; } + public string? LastName { get; set; } + + [Required(ErrorMessage = "Email is required")] + [EmailAddress] + public string? Email { get; set; } + + [Required(ErrorMessage = "Password is required")] + [DataType(DataType.Password)] + public string? Password { get; set; } + + [Required(ErrorMessage = "Password is required")] + [DataType(DataType.Password)] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string? ConfirmPassword { get; set; } + } +} diff --git a/src/Api/Insight.Api/Program.cs b/src/Api/Insight.Api/Program.cs new file mode 100644 index 0000000..0c9e95f --- /dev/null +++ b/src/Api/Insight.Api/Program.cs @@ -0,0 +1,99 @@ +using Insight.Api.Hosting; +using Insight.Domain.Constants; +using Insight.Infrastructure; +using Microsoft.Extensions.FileProviders; + +namespace Insight.Api +{ + public class Program + { + public static async Task Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + builder.Host.UseWindowsService(); + builder.Host.UseSystemd(); + + // LOGGER + builder.Logging.ClearProviders(); + builder.Logging.SetMinimumLevel(LogLevel.Trace); + builder.Logging.AddFilter("Microsoft.AspNetCore", LogLevel.Warning); + + builder.Logging.AddSimpleConsole(options => + { + options.IncludeScopes = true; + options.SingleLine = true; + options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff "; + }); + + builder.Logging.AddFile($"{Configuration.AppDirectory?.FullName}/" + "logs/api_{Date}.log", LogLevel.Trace, fileSizeLimitBytes: 104857600, retainedFileCountLimit: 10, outputTemplate: "{Timestamp:o} [{Level:u3}] {Message} {NewLine}{Exception}"); + + // INFRASTRUCTURE + builder.Services.AddDatabase(builder.Configuration); + builder.Services.AddInfrastructureServices(); + + // IDENTITY + builder.Services.AddIdentityServices(builder.Configuration); + builder.Services.AddBearerAuthentication(builder.Configuration); + builder.Services.AddTokenServices(builder.Configuration); + + // SECURITY + builder.Services.AddAuthorization(); + + // WEBSERVICES + builder.Services.AddProxyServices(builder.Configuration); + builder.Services.AddRoutingServices(builder.Configuration); + builder.Services.AddControllers(); + + // SWAGGER + builder.Services.AddSwaggerServices(builder.Configuration); + + //builder.Services.AddControllers(); + //builder.Services.AddEndpointsApiExplorer(); + //builder.Services.AddSwaggerGen(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + app.UseForwardedHeaders(); + + if (app.Environment.IsDevelopment()) + { + + } + + app.UseSwagger(options => + { + options.RouteTemplate = "api/swagger/{documentName}/swagger.json"; + }); + + app.UseSwaggerUI(options => + { + options.DefaultModelsExpandDepth(-1); + options.SwaggerEndpoint("/api/swagger/v1/swagger.json", "v1"); + options.RoutePrefix = "api/swagger"; + }); + + app.UseCors(x => x + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + + app.UseAuthorization(); + + app.MapControllers(); + + // STATIC FILES (UPDATES) + var files = new DirectoryInfo($"{Domain.Constants.Configuration.AppDirectory?.FullName}/files"); + files.Create(); + + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider(files.FullName), + RequestPath = "/api/files", + ServeUnknownFileTypes = true + }); + + await app.RunAsync(); + } + } +} \ No newline at end of file diff --git a/src/Api/Insight.Api/Properties/launchSettings.json b/src/Api/Insight.Api/Properties/launchSettings.json new file mode 100644 index 0000000..9536342 --- /dev/null +++ b/src/Api/Insight.Api/Properties/launchSettings.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "Development": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "api/swagger", + "applicationUrl": "http://localhost:5003", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Production": { + "commandName": "Project", + "applicationUrl": "http://localhost:5001" + } + } +} diff --git a/src/Api/Insight.Api/appsettings.Development.json b/src/Api/Insight.Api/appsettings.Development.json new file mode 100644 index 0000000..fff92f2 --- /dev/null +++ b/src/Api/Insight.Api/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "AllowedHosts": "*", + "Urls": "http://127.0.0.1:5000", + "database": "mongodb://db.insight.local:27017", + "jwt.key": "x5dcaE8fiBmHfgsNrwIEtSWzZkz6gpouzKOIgEiVjxJnW28V1aUnYXF19IcnF5x", + "jwt.exp": 3600, + "jwt.audience": "http://127.0.0.1:5000", + "jwt.issuer": "http://127.0.0.1:5000" +} \ No newline at end of file diff --git a/src/Api/Insight.Api/appsettings.json b/src/Api/Insight.Api/appsettings.json new file mode 100644 index 0000000..84eb581 --- /dev/null +++ b/src/Api/Insight.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "AllowedHosts": "*", + "Urls": "http://127.0.0.1:5000", + "database": "mongodb://127.0.0.1:27017", + "jwt.key": "x5dcaE8fiBmHfgsNrwIEtSWzZkz6gpouzKOIgEiVjxJnW28V1aUnYXF19IcnF5x", + "jwt.exp": 3600, + "jwt.audience": "https://insight.webmatic.de/api", + "jwt.issuer": "https://insight.webmatic.de/api" +} diff --git a/src/Core/Insight.Domain/Constants/Configuration.cs b/src/Core/Insight.Domain/Constants/Configuration.cs new file mode 100644 index 0000000..f0643e2 --- /dev/null +++ b/src/Core/Insight.Domain/Constants/Configuration.cs @@ -0,0 +1,13 @@ +using System.Net; +using System.Reflection; + +namespace Insight.Domain.Constants +{ + public static class Configuration + { + public static string Hostname => Dns.GetHostEntry("localhost").HostName; + public static Version Version => Assembly.GetEntryAssembly()?.GetName().Version ?? throw new ArgumentNullException("Version"); + public static DirectoryInfo? AppDirectory => string.IsNullOrWhiteSpace(Environment.ProcessPath) ? null : new DirectoryInfo(Environment.ProcessPath).Parent; + public static string DefaultConfig => Path.Combine(AppDirectory?.FullName ?? string.Empty, "config.json"); + } +} \ No newline at end of file diff --git a/src/Core/Insight.Domain/Insight.Domain.csproj b/src/Core/Insight.Domain/Insight.Domain.csproj new file mode 100644 index 0000000..7386681 --- /dev/null +++ b/src/Core/Insight.Domain/Insight.Domain.csproj @@ -0,0 +1,20 @@ + + + + net7.0 + true + enable + Insight.Domain + Insight + 2023.7.3.0 + + + + none + + + + none + + + diff --git a/src/Core/Insight.Domain/Models/Token.cs b/src/Core/Insight.Domain/Models/Token.cs new file mode 100644 index 0000000..66d91bc --- /dev/null +++ b/src/Core/Insight.Domain/Models/Token.cs @@ -0,0 +1,55 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Insight.Domain.Models +{ + public class TokenRequest + { + [JsonPropertyName("username"), EmailAddress, Required] + public string Username { get; set; } + + [JsonPropertyName("password"), DataType(DataType.Password), Required] + public string Password { get; set; } + + [JsonPropertyName("code"), DataType(DataType.Text)] + public string? Code { get; set; } + } + + public class TokenResponse + { + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } + + [JsonPropertyName("expires_in")] + public int ExpireInSeconds { get; set; } + + [JsonPropertyName("refresh_token")] + public string RefreshToken { get; set; } + } + + public class TokenRevokeRequest + { + [JsonPropertyName("token"), Required] + public string Token { get; set; } + + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + public TokenRevokeRequest(string token, string? reason) + { + Token = token; + Reason = reason; + } + } + + public class TokenRefreshRequest + { + [JsonPropertyName("token"), Required] + public string Token { get; set; } + + public TokenRefreshRequest(string token) + { + Token = token; + } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Domain/Models/Update.cs b/src/Core/Insight.Domain/Models/Update.cs new file mode 100644 index 0000000..6f0fd0b --- /dev/null +++ b/src/Core/Insight.Domain/Models/Update.cs @@ -0,0 +1,8 @@ +namespace Insight.Domain.Models +{ + public class UpdateResponse + { + public Version? Version { get; set; } + public Uri? Uri { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Constants/Appsettings.cs b/src/Core/Insight.Infrastructure/Constants/Appsettings.cs new file mode 100644 index 0000000..d85fd32 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Constants/Appsettings.cs @@ -0,0 +1,14 @@ +namespace Insight.Infrastructure +{ + public class Appsettings + { + public const string Database = "database"; + public const string JwtKey = "jwt.key"; + public const string JwtAudience = "jwt.audience"; + public const string JwtIssuer = "jwt.issuer"; + public const string JwtExp = "jwt.exp"; + + public const string ServerHost = "server.host"; + public const string ServerPort = "server.port"; + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Constants/Monitoring.cs b/src/Core/Insight.Infrastructure/Constants/Monitoring.cs new file mode 100644 index 0000000..eb6c355 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Constants/Monitoring.cs @@ -0,0 +1,8 @@ +namespace Insight.Infrastructure +{ + public static class Monitoring + { + public static readonly Uri StatusUri = new("https://admin.webmatic.de/monitoring/computer/send/status"); + public static readonly Uri LogUri = new("https://admin.webmatic.de/monitoring/computer/send/log"); + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Constants/Settings.cs b/src/Core/Insight.Infrastructure/Constants/Settings.cs new file mode 100644 index 0000000..b667086 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Constants/Settings.cs @@ -0,0 +1,7 @@ +namespace Insight.Infrastructure +{ + public class Settings + { + public const string Database = "insight"; + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/Agent.cs b/src/Core/Insight.Infrastructure/Entities/Agent.cs new file mode 100644 index 0000000..d538498 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/Agent.cs @@ -0,0 +1,62 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class AgentEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("serial"), Required] + public string? Serial { get; set; } + + [BsonElement("hostname")] + public string? Hostname { get; set; } + + [BsonElement("version"), BsonRepresentation(BsonType.String)] + public Version? Version { get; set; } + + [BsonElement("endpoint"), BsonRepresentation(BsonType.String)] + public string? Endpoint { get; set; } + + [BsonElement("connected")] + public DateTime? Connected { get; set; } + + [BsonElement("activity")] + public DateTime? Activity { get; set; } + + [BsonElement("bytes_sent")] + public long SentBytes { get; set; } + + [BsonElement("bytes_received")] + public long ReceivedBytes { get; set; } + + [BsonElement("packets_sent")] + public long SentPackets { get; set; } + + [BsonElement("packets_received")] + public long ReceivedPackets { get; set; } + + //[BsonElement("latency")] + //public TimeSpan? Latency { get; set; } + + [BsonIgnoreIfNull, JsonIgnore] + public List? Hosts { get; set; } + + public bool GetOnlineState() + { + if (Activity is null) return false; + return Activity.Value.ToLocalTime().Add(TimeSpan.FromSeconds(60)).Subtract(DateTime.Now).Seconds > 0; + } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/AgentLog.cs b/src/Core/Insight.Infrastructure/Entities/AgentLog.cs new file mode 100644 index 0000000..8c32884 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/AgentLog.cs @@ -0,0 +1,37 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class AgentLogEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_agent"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("agent")] + public string? Agent { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("eventid")] + public string? EventId { get; set; } + + [BsonElement("status")] + public string? Status { get; set; } + + [BsonElement("source")] + public string? Source { get; set; } + + [BsonElement("category")] + public string? Category { get; set; } + + [BsonElement("message")] + public string? Message { get; set; } + + [BsonElement("timestamp")] + public DateTime? Timestamp { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/Customer.cs b/src/Core/Insight.Infrastructure/Entities/Customer.cs new file mode 100644 index 0000000..983715e --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/Customer.cs @@ -0,0 +1,29 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class CustomerEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("name"), Required] + public string? Name { get; set; } + + [BsonElement("tag")] + public string? Tag { get; set; } + + [BsonIgnoreIfNull, JsonIgnore] + public List? Hosts { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/Host.cs b/src/Core/Insight.Infrastructure/Entities/Host.cs new file mode 100644 index 0000000..9297e78 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/Host.cs @@ -0,0 +1,38 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_customer"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("customer")] + public string? Customer { get; set; } + + [BsonElement("_agent"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("agent")] + public string? Agent { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("name"), Required] + public string? Name { get; set; } + + [BsonElement("description")] + public string? Description { get; set; } + + [BsonIgnoreIfNull, JsonIgnore] + public List? Customers { get; set; } + + [BsonIgnoreIfNull, JsonIgnore] + public List? Agents { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostApplication.cs b/src/Core/Insight.Infrastructure/Entities/HostApplication.cs new file mode 100644 index 0000000..473f383 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostApplication.cs @@ -0,0 +1,40 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostApplicationEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("company")] + public string? Company { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("architecture")] + public string? Architecture { get; set; } + + [BsonElement("version")] + public string? Version { get; set; } + + [BsonElement("installdate")] + public DateTime? InstallDate { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostDrive.cs b/src/Core/Insight.Infrastructure/Entities/HostDrive.cs new file mode 100644 index 0000000..f8cde99 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostDrive.cs @@ -0,0 +1,52 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostDriveEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("index")] + public uint? Index { get; set; } + + [BsonElement("company")] + public string? Company { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("size")] + public ulong? Size { get; set; } + + [BsonElement("type")] + public string? Type { get; set; } + + [BsonElement("serial")] + public string? Serial { get; set; } + + [BsonElement("firmware")] + public string? Firmware { get; set; } + + [BsonElement("status")] + public string? Status { get; set; } + + [BsonElement("pnp")] + public string? Pnp { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostHypervisor.cs b/src/Core/Insight.Infrastructure/Entities/HostHypervisor.cs new file mode 100644 index 0000000..a51b202 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostHypervisor.cs @@ -0,0 +1,188 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostHypervisorVirtualMaschineEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("uniqueid")] + public string? UniqueId { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("notes")] + public string? Notes { get; set; } + + [BsonElement("enabled")] + public string? Enabled { get; set; } + + [BsonElement("enabled_default")] + public string? EnabledDefault { get; set; } + + [BsonElement("health")] + public string? Health { get; set; } + + [BsonElement("status")] + public string? Status { get; set; } + + [BsonElement("ontime")] + public ulong? OnTime { get; set; } + + [BsonElement("replication_state")] + public string? ReplicationState { get; set; } + + [BsonElement("replication_health")] + public string? ReplicationHealth { get; set; } + + [BsonElement("version_configuration")] + public string? ConfigurationVersion { get; set; } + + [BsonElement("version_integrated_services")] + public string? IntegrationServicesVersionState { get; set; } + + [BsonElement("processid")] + public uint? ProcessId { get; set; } + + [BsonElement("processor_count")] + public uint? NumberOfProcessors { get; set; } + + [BsonElement("processor_load")] + public uint? ProcessorLoad { get; set; } + + [BsonElement("memory_available")] + public int? MemoryAvailable { get; set; } + + [BsonElement("memory_usage")] + public ulong? MemoryUsage { get; set; } + + [BsonElement("installdate")] + public DateTime? InstallDate { get; set; } + + [BsonElement("configuration_changed")] + public DateTime? TimeOfLastConfigurationChange { get; set; } + + [BsonElement("state_changed")] + public DateTime? TimeOfLastStateChange { get; set; } + + [BsonElement("replication_last")] + public DateTime? LastReplicationTime { get; set; } + + [BsonElement("guest_os")] + public string? Os { get; set; } + + [BsonIgnoreIfNull, JsonIgnore] + public List? Configs { get; set; } + } + + [BsonIgnoreExtraElements] + public class HostHypervisorVirtualMaschineConfigEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("virtualmaschine"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("virtualmaschine")] + public string? VirtualMaschine { get; set; } + + [BsonElement("batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("uniqueid")] + public string? UniqueId { get; set; } + + [BsonElement("parentid")] + public string? ParentId { get; set; } + + [BsonElement("type")] + public string? Type { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("notes")] + public string? Notes { get; set; } + + [BsonElement("creationtime")] + public DateTime? CreationTime { get; set; } + + [BsonElement("generation")] + public string? Generation { get; set; } + + [BsonElement("architecture")] + public string? Architecture { get; set; } + + [BsonElement("secureboot")] + public bool? SecureBootEnabled { get; set; } + + [BsonElement("automatic_snapshot")] + public bool? IsAutomaticSnapshot { get; set; } + + [BsonElement("action_start")] + public string? AutomaticStartupAction { get; set; } + + [BsonElement("action_shutdown")] + public string? AutomaticShutdownAction { get; set; } + + [BsonElement("action_recovery")] + public string? AutomaticRecoveryAction { get; set; } + + [BsonElement("auto_snapshots")] + public bool? AutomaticSnapshotsEnabled { get; set; } + + [BsonElement("serial_mainboard")] + public string? BaseBoardSerialNumber { get; set; } + + [BsonElement("serial_bios")] + public string? BIOSSerialNumber { get; set; } + + [BsonElement("bios_guid")] + public string? BIOSGUID { get; set; } + + [BsonElement("data_root")] + public string? ConfigurationDataRoot { get; set; } + + [BsonElement("file")] + public string? ConfigurationFile { get; set; } + + [BsonElement("guest_data_root")] + public string? GuestStateDataRoot { get; set; } + + [BsonElement("guest_state_file")] + public string? GuestStateFile { get; set; } + + [BsonElement("snapshot_data_root")] + public string? SnapshotDataRoot { get; set; } + + [BsonElement("suspend_data_root")] + public string? SuspendDataRoot { get; set; } + + [BsonElement("swap_data_root")] + public string? SwapFileDataRoot { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostInterface.cs b/src/Core/Insight.Infrastructure/Entities/HostInterface.cs new file mode 100644 index 0000000..2b18c18 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostInterface.cs @@ -0,0 +1,218 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostInterfaceEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("index")] + public uint? Index { get; set; } + + [BsonElement("mac")] + public string? Mac { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("description")] + public string? Description { get; set; } + + [BsonElement("physical")] + public bool? Physical { get; set; } + + [BsonElement("status")] + public string? Status { get; set; } + + [BsonElement("suffix")] + public string? Suffix { get; set; } + + [BsonElement("speed")] + public long? Speed { get; set; } + + [BsonElement("ipv4_mtu")] + public long? Ipv4Mtu { get; set; } + + [BsonElement("ipv4_dhcp")] + public bool? Ipv4Dhcp { get; set; } + + [BsonElement("ipv4_forwarding")] + public bool? Ipv4Forwarding { get; set; } + + [BsonElement("ipv6_mtu")] + public long? Ipv6Mtu { get; set; } + + [BsonElement("sent")] + public long? Sent { get; set; } + + [BsonElement("received")] + public long? Received { get; set; } + + [BsonElement("packets_incoming_discarded")] + public long? IncomingPacketsDiscarded { get; set; } + + [BsonElement("packets_incoming_errors")] + public long? IncomingPacketsWithErrors { get; set; } + + [BsonElement("packets_incoming_unknown")] + public long? IncomingUnknownProtocolPackets { get; set; } + + [BsonElement("packets_outgoing_discarded")] + public long? OutgoingPacketsDiscarded { get; set; } + + [BsonElement("packets_outgoing_errors")] + public long? OutgoingPacketsWithErrors { get; set; } + } + + [BsonIgnoreExtraElements] + public class HostInterfaceAddressEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_interface"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("interface")] + public string? Interface { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("address")] + public string? Address { get; set; } + + [BsonElement("mask")] + public string? Mask { get; set; } + + //public string? State { get; set; } + //public long? PreferredLifetime { get; set; } + //public long? ValidLifetime { get; set; } + //public long? LeaseLifetime { get; set; } + //public int? PrefixLength { get; set; } + + [BsonIgnoreIfNull, JsonIgnore] + public List? Interfaces { get; set; } + } + + [BsonIgnoreExtraElements] + public class HostInterfaceGatewayEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_interface"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("interface")] + public string? Interface { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("address")] + public string? Address { get; set; } + + [BsonElement("mask")] + public string? Mask { get; set; } + + [BsonIgnoreIfNull, JsonIgnore] + public List? Interfaces { get; set; } + } + + [BsonIgnoreExtraElements] + public class HostInterfaceNameserverEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_interface"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("interface")] + public string? Interface { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("address")] + public string? Address { get; set; } + + [BsonElement("mask")] + public string? Mask { get; set; } + + [BsonIgnoreIfNull, JsonIgnore] + public List? Interfaces { get; set; } + } + + [BsonIgnoreExtraElements] + public class HostInterfaceRouteEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_interface"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("interface")] + public string? Interface { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("destination")] + public string? Destination { get; set; } + + [BsonElement("mask")] + public string? Mask { get; set; } + + [BsonElement("gateway")] + public string? Gateway { get; set; } + + [BsonElement("metric")] + public int? Metric { get; set; } + + [BsonIgnoreIfNull, JsonIgnore] + public List? Interfaces { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostLog.cs b/src/Core/Insight.Infrastructure/Entities/HostLog.cs new file mode 100644 index 0000000..c6d4ae4 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostLog.cs @@ -0,0 +1,37 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostLogEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("eventid")] + public string? EventId { get; set; } + + [BsonElement("status")] + public string? Status { get; set; } + + [BsonElement("source")] + public string? Source { get; set; } + + [BsonElement("category")] + public string? Category { get; set; } + + [BsonElement("message")] + public string? Message { get; set; } + + [BsonElement("timestamp")] + public DateTime? Timestamp { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostLogMonitoring.cs b/src/Core/Insight.Infrastructure/Entities/HostLogMonitoring.cs new file mode 100644 index 0000000..67bf429 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostLogMonitoring.cs @@ -0,0 +1,43 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostLogMonitoringEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("hostname")] + public string? Hostname { get; set; } + + [BsonElement("category")] + public string? Category { get; set; } + + [BsonElement("status")] + public string? Status { get; set; } + + [BsonElement("task")] + public string? Task { get; set; } + + [BsonElement("message")] + public string? Message { get; set; } + + [BsonElement("dispatch")] + public string? Dispatch { get; set; } + + [BsonElement("timestamp")] + public DateTime? Timestamp { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostMainboard.cs b/src/Core/Insight.Infrastructure/Entities/HostMainboard.cs new file mode 100644 index 0000000..f4fd655 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostMainboard.cs @@ -0,0 +1,40 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostMainboardEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("company")] + public string? Company { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("serial")] + public string? Serial { get; set; } + + [BsonElement("bios")] + public string? Bios { get; set; } + + [BsonElement("version")] + public string? Version { get; set; } + + [BsonElement("Date")] + public DateTime? Date { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostMemory.cs b/src/Core/Insight.Infrastructure/Entities/HostMemory.cs new file mode 100644 index 0000000..1442941 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostMemory.cs @@ -0,0 +1,58 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostMemoryEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("index")] + public uint? Index { get; set; } + + [BsonElement("company")] + public string? Company { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("tag")] + public string? Tag { get; set; } + + [BsonElement("location")] + public string? Location { get; set; } + + [BsonElement("serial")] + public string? Serial { get; set; } + + [BsonElement("capacity")] + public ulong? Capacity { get; set; } + + [BsonElement("clock")] + public uint? Clock { get; set; } + + [BsonElement("clock_current")] + public uint? CurrentClock { get; set; } + + [BsonElement("voltage")] + public uint? Voltage { get; set; } + + [BsonElement("voltage_current")] + public uint? CurrentVoltage { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostOs.cs b/src/Core/Insight.Infrastructure/Entities/HostOs.cs new file mode 100644 index 0000000..b0e0d8f --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostOs.cs @@ -0,0 +1,40 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostOsEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("version")] + public string? Version { get; set; } + + [BsonElement("architecture")] + public string? Architecture { get; set; } + + [BsonElement("serialnumber")] + public string? SerialNumber { get; set; } + + [BsonElement("virtual")] + public bool? Virtual { get; set; } + + [BsonElement("installed")] + public DateTime? Installed { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostPrinter.cs b/src/Core/Insight.Infrastructure/Entities/HostPrinter.cs new file mode 100644 index 0000000..7131499 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostPrinter.cs @@ -0,0 +1,40 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostPrinterEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("company")] + public string? Company { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("port")] + public string? Port { get; set; } + + [BsonElement("location")] + public string? Location { get; set; } + + [BsonElement("comment")] + public string? Comment { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostProcessor.cs b/src/Core/Insight.Infrastructure/Entities/HostProcessor.cs new file mode 100644 index 0000000..1a81ceb --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostProcessor.cs @@ -0,0 +1,70 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostProcessorEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("index")] + public uint? Index { get; set; } + + [BsonElement("company")] + public string? Company { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("socket")] + public string? Socket { get; set; } + + [BsonElement("serial")] + public string? Serial { get; set; } + + [BsonElement("version")] + public string? Version { get; set; } + + [BsonElement("cores")] + public uint? Cores { get; set; } + + [BsonElement("logicalcores")] + public uint? LogicalCores { get; set; } + + [BsonElement("clock")] + public uint? Clock { get; set; } + + [BsonElement("clock_current")] + public uint? CurrentClock { get; set; } + + [BsonElement("l1")] + public uint? L1Size { get; set; } + + [BsonElement("l2")] + public uint? L2Size { get; set; } + + [BsonElement("l3")] + public uint? L3Size { get; set; } + + [BsonElement("virtualization")] + public bool? Virtualization { get; set; } + + [BsonElement("pnp")] + public string? PNP { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostService.cs b/src/Core/Insight.Infrastructure/Entities/HostService.cs new file mode 100644 index 0000000..c2e71d7 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostService.cs @@ -0,0 +1,55 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostServiceEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("company")] + public string? Company { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("displayname")] + public string? DisplayName { get; set; } + + [BsonElement("description")] + public string? Description { get; set; } + + [BsonElement("startmode")] + public string? StartMode { get; set; } + + [BsonElement("state")] + public string? State { get; set; } + + [BsonElement("processid")] + public uint? ProcessId { get; set; } + + [BsonElement("delay")] + public bool? Delay { get; set; } + + [BsonElement("path")] + public string? Path { get; set; } + + [BsonElement("account")] + public string? Account { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostSession.cs b/src/Core/Insight.Infrastructure/Entities/HostSession.cs new file mode 100644 index 0000000..7c58585 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostSession.cs @@ -0,0 +1,40 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostSessionEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("sid")] + public string? Sid { get; set; } + + [BsonElement("user")] + public string? User { get; set; } + + [BsonElement("remote")] + public string? Remote { get; set; } + + [BsonElement("type")] + public string? Type { get; set; } + + [BsonElement("state")] + public string? State { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostStoragePool.cs b/src/Core/Insight.Infrastructure/Entities/HostStoragePool.cs new file mode 100644 index 0000000..67dd652 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostStoragePool.cs @@ -0,0 +1,207 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostStoragePoolEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("uniqueid")] + public string? UniqueId { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("health")] + public string? Health { get; set; } + + [BsonElement("resiliency")] + public string? Resiliency { get; set; } + + [BsonElement("primordial")] + public bool? Primordial { get; set; } + + [BsonElement("readonly")] + public bool? ReadOnly { get; set; } + + [BsonElement("clustered")] + public bool? Clustered { get; set; } + + [BsonElement("size")] + public ulong? Size { get; set; } + + [BsonElement("size_allocated")] + public ulong? AllocatedSize { get; set; } + + [BsonElement("sectorsize")] + public ulong? SectorSize { get; set; } + + [BsonElement("states")] + public List? States { get; set; } + + [BsonIgnoreIfNull, JsonIgnore] + public List? PhysicalDisks { get; set; } + + [BsonIgnoreIfNull, JsonIgnore] + public List? VirtualDisks { get; set; } + } + + [BsonIgnoreExtraElements] + public class HostStoragePoolVirtualDiskEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_storagepool"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("storagepool")] + public string? StoragePool { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("uniqueid")] + public string? UniqueId { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("health")] + public string? Health { get; set; } + + [BsonElement("access")] + public string? Access { get; set; } + + [BsonElement("provisioning")] + public string? Provisioning { get; set; } + + [BsonElement("redundancy")] + public uint? PhysicalRedundancy { get; set; } + + [BsonElement("resiliency")] + public string? Resiliency { get; set; } + + [BsonElement("deduplication")] + public bool? Deduplication { get; set; } + + [BsonElement("snapshot")] + public bool? Snapshot { get; set; } + + [BsonElement("size")] + public ulong? Size { get; set; } + + [BsonElement("size_allocated")] + public ulong? AllocatedSize { get; set; } + + [BsonElement("footprint")] + public ulong? Footprint { get; set; } + + [BsonElement("cache_read_size")] + public ulong? ReadCacheSize { get; set; } + + [BsonElement("cache_write_size")] + public ulong? WriteCacheSize { get; set; } + + [BsonElement("states")] + public List? States { get; set; } + } + + [BsonIgnoreExtraElements] + public class HostStoragePoolPhysicalDiskEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_storagepool"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("storagepool")] + public string? StoragePool { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("uniqueid")] + public string? UniqueId { get; set; } + + [BsonElement("deviceid")] + public string? DeviceId { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("manufacturer")] + public string? Manufacturer { get; set; } + + [BsonElement("Model")] + public string? Model { get; set; } + + [BsonElement("media")] + public string? Media { get; set; } + + [BsonElement("bus")] + public string? Bus { get; set; } + + [BsonElement("health")] + public string? Health { get; set; } + + [BsonElement("usage")] + public ushort? Usage { get; set; } + + [BsonElement("location")] + public string? Location { get; set; } + + [BsonElement("serial")] + public string? Serial { get; set; } + + [BsonElement("firmware")] + public string? Firmware { get; set; } + + [BsonElement("size")] + public ulong? Size { get; set; } + + [BsonElement("size_allocated")] + public ulong? AllocatedSize { get; set; } + + [BsonElement("footprint")] + public ulong? Footprint { get; set; } + + [BsonElement("sector_size_physical")] + public ulong? PhysicalSectorSize { get; set; } + + [BsonElement("sector_size_logical")] + public ulong? LogicalSectorSize { get; set; } + + [BsonElement("states")] + public List? States { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostSystem.cs b/src/Core/Insight.Infrastructure/Entities/HostSystem.cs new file mode 100644 index 0000000..7cea81e --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostSystem.cs @@ -0,0 +1,34 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostSystemEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("localtime")] + public DateTime? LocalTime { get; set; } + + [BsonElement("bootuptime")] + public DateTime? BootUpTime { get; set; } + + [BsonElement("processes")] + public uint? Processes { get; set; } + + [BsonElement("license")] + public string? License { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostUpdate.cs b/src/Core/Insight.Infrastructure/Entities/HostUpdate.cs new file mode 100644 index 0000000..2e2e545 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostUpdate.cs @@ -0,0 +1,61 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostUpdateEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("serial")] + public string? Serial { get; set; } // os update id + + [BsonElement("description")] + public string? Description { get; set; } + + [BsonElement("supporturl")] + public string? SupportUrl { get; set; } + + [BsonElement("date")] + public DateTime? Date { get; set; } + + [BsonElement("pending")] + public bool? Pending { get; set; } + + [BsonElement("result"), BsonIgnoreIfNull] // installed only + public string? Result { get; set; } + + [BsonElement("type"), BsonIgnoreIfNull] // pending only + public string? Type { get; set; } + + [BsonElement("size"), BsonIgnoreIfNull] // pending only + public decimal? Size { get; set; } + + [BsonElement("downloaded"), BsonIgnoreIfNull] // pending only + public bool? IsDownloaded { get; set; } + + [BsonElement("inputrequest"), BsonIgnoreIfNull] // pending only + public bool? CanRequestUserInput { get; set; } + + [BsonElement("reboot"), BsonIgnoreIfNull] // pending only + public string? RebootBehavior { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostUser.cs b/src/Core/Insight.Infrastructure/Entities/HostUser.cs new file mode 100644 index 0000000..7ef4201 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostUser.cs @@ -0,0 +1,120 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostUserEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("sid")] + public string? Sid { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("domain")] + public string? Domain { get; set; } + + [BsonElement("fullname")] + public string? FullName { get; set; } + + [BsonElement("description")] + public string? Description { get; set; } + + [BsonElement("status")] + public string? Status { get; set; } + + [BsonElement("localaccount")] + public bool? LocalAccount { get; set; } + + [BsonElement("disabled")] + public bool? Disabled { get; set; } + + [BsonElement("lockout")] + public bool? Lockout { get; set; } + + [BsonElement("password_changeable")] + public bool? PasswordChangeable { get; set; } + + [BsonElement("password_expires")] + public bool? PasswordExpires { get; set; } + + [BsonElement("password_required")] + public bool? PasswordRequired { get; set; } + } + + [BsonIgnoreExtraElements] + public class HostGroupEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("sid")] + public string? Sid { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("domain")] + public string? Domain { get; set; } + + [BsonElement("description")] + public string? Description { get; set; } + + [BsonElement("localaccount")] + public bool? LocalAccount { get; set; } + } + + [BsonIgnoreExtraElements] + public class HostUserGroupEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_user"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("user")] + public string? User { get; set; } + + [BsonElement("_group"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("group")] + public string? Group { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostVideocard.cs b/src/Core/Insight.Infrastructure/Entities/HostVideocard.cs new file mode 100644 index 0000000..a2552a9 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostVideocard.cs @@ -0,0 +1,40 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostVideocardEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("company")] + public string? Company { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("memory")] + public ulong? Memory { get; set; } + + [BsonElement("driver")] + public string? Driver { get; set; } + + [BsonElement("date")] + public DateTime? Date { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/HostVolume.cs b/src/Core/Insight.Infrastructure/Entities/HostVolume.cs new file mode 100644 index 0000000..00c19df --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/HostVolume.cs @@ -0,0 +1,76 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [BsonIgnoreExtraElements] + public class HostVolumeEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_host"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("host")] + public string? Host { get; set; } + + [BsonElement("_drive"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("drive")] + public string? Drive { get; set; } + + [BsonElement("_batch"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("batch")] + public string? Batch { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("index")] + public uint? Index { get; set; } + + [BsonElement("label")] + public string? Label { get; set; } + + [BsonElement("name")] + public string? Name { get; set; } + + [BsonElement("serial")] + public string? Serial { get; set; } + + [BsonElement("size")] + public ulong? Size { get; set; } + + [BsonElement("freespace")] + public ulong? FreeSpace { get; set; } + + [BsonElement("type")] + public string? Type { get; set; } + + [BsonElement("filesystem")] + public string? FileSystem { get; set; } + + [BsonElement("compressed")] + public bool? Compressed { get; set; } + + [BsonElement("bootable")] + public bool? Bootable { get; set; } + + [BsonElement("primary")] + public bool? Primary { get; set; } + + [BsonElement("boot")] + public bool? Boot { get; set; } + + [BsonElement("blocksize")] + public ulong? BlockSize { get; set; } + + [BsonElement("blocks")] + public ulong? Blocks { get; set; } + + [BsonElement("startoffset")] + public ulong? StartingOffset { get; set; } + + [BsonElement("provider")] + public string? Provider { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Entities/Identity.cs b/src/Core/Insight.Infrastructure/Entities/Identity.cs new file mode 100644 index 0000000..9827a9b --- /dev/null +++ b/src/Core/Insight.Infrastructure/Entities/Identity.cs @@ -0,0 +1,99 @@ +using AspNetCore.Identity.MongoDbCore.Models; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDbGenericRepository.Attributes; +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Entities +{ + [CollectionName("user"), BsonIgnoreExtraElements] + public class InsightUser : MongoIdentityUser + { + public InsightUser() : base() { } + + public InsightUser(string userName, string email) : base(userName, email) { } + + [JsonPropertyName("refresh_tokens")] + public List? RefreshTokens { get; set; } + } + + [BsonIgnoreExtraElements] + public class InsightUserLogEntity + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_user"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("user")] + public string? User { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("timestamp")] + public DateTime? Timestamp { get; set; } + + [BsonElement("message")] + public string? Message { get; set; } + } + + [CollectionName("user_pref"), BsonIgnoreExtraElements] + public class InsightUserPreferences + { + [BsonId, BsonRepresentation(BsonType.ObjectId), JsonPropertyName("id")] + public string? Id { get; set; } + + [BsonElement("_user"), BsonRepresentation(BsonType.ObjectId), JsonPropertyName("user")] + public string? User { get; set; } + + [BsonElement("insert")] + public DateTime? Insert { get; set; } + + [BsonElement("update")] + public DateTime? Update { get; set; } + + [BsonElement("darkmode")] + public bool DarkMode { get; set; } + } + + [CollectionName("role")] + public class InsightRole : MongoIdentityRole + { + public InsightRole() : base() { } + + public InsightRole(string roleName) : base(roleName) { } + } + + [BsonIgnoreExtraElements] + public class RefreshToken + { + [BsonElement("token")] + public string? Token { get; set; } + + [BsonElement("created")] + public DateTime Created { get; set; } + + [BsonElement("created_ip")] + public string? CreatedByIp { get; set; } + + [BsonElement("expires")] + public DateTime Expires { get; set; } + + [BsonElement("revoked")] + public DateTime? Revoked { get; set; } + + [BsonElement("revoked_ip")] + public string? RevokedByIp { get; set; } + + [BsonElement("revoked_reason")] + public string? ReasonRevoked { get; set; } + + [BsonIgnore, JsonIgnore] + public bool IsExpired => DateTime.Now >= Expires.ToLocalTime(); + + [BsonIgnore, JsonIgnore] + public bool IsRevoked => Revoked != null; + + [BsonIgnore, JsonIgnore] + public bool IsActive => !IsRevoked && !IsExpired; + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Extensions/HttpRequestExtensions.cs b/src/Core/Insight.Infrastructure/Extensions/HttpRequestExtensions.cs new file mode 100644 index 0000000..27d0cb9 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Extensions/HttpRequestExtensions.cs @@ -0,0 +1,47 @@ +using Insight.Infrastructure.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; + +namespace Insight.Infrastructure +{ + public static class HttpRequestExtensions + { + public static void AddPagination(this HttpRequest request, PagedList pagelist) + { + var builder = new QueryBuilder(); + + foreach (var item in request.Query.Where(p => p.Key.ToLower() != "limit" || p.Key.ToLower() != "offset")) + { + builder.Add(item.Key.ToLower(), item.Value.ToString()); + } + + builder.Add("limit", pagelist.Meta.Limit.ToString()); + + if (pagelist.Meta.Offset > 0) + { + var qb = new QueryBuilder(builder); + + if (pagelist.Meta.Offset > pagelist.Meta.Limit) + { + qb.Add("offset", (pagelist.Meta.Offset - pagelist.Meta.Limit).ToString()); + } + else + { + qb.Add("offset", 0.ToString()); + } + + pagelist.Meta.Previous = $"{request.Scheme}://{request.Host}{request.PathBase}{request.Path}{qb}"; + } + + if ((pagelist.Meta.Offset + pagelist.Meta.Count) < pagelist.Meta.Total) + { + var qb = new QueryBuilder(builder) + { + { "offset", (pagelist.Meta.Offset + pagelist.Meta.Count).ToString() } + }; + + pagelist.Meta.Next = $"{request.Scheme}://{request.Host}{request.PathBase}{request.Path}{qb}"; + } + } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Extensions/HttpResponseExtensions.cs b/src/Core/Insight.Infrastructure/Extensions/HttpResponseExtensions.cs new file mode 100644 index 0000000..6bb788d --- /dev/null +++ b/src/Core/Insight.Infrastructure/Extensions/HttpResponseExtensions.cs @@ -0,0 +1,17 @@ +using Insight.Infrastructure.Models; +using Microsoft.AspNetCore.Http; +using System.Text.Json; + +namespace Insight.Infrastructure +{ + public static class HttpResponseExtensions + { + public static void AddPagination(this HttpResponse response, PagedList pagelist) + { + response.Headers.Add("X-Pagination", JsonSerializer.Serialize(pagelist.Meta as PagedHeaderData, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + })); + } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Extensions/MongoCollectionExtensions.cs b/src/Core/Insight.Infrastructure/Extensions/MongoCollectionExtensions.cs new file mode 100644 index 0000000..bc18ab8 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Extensions/MongoCollectionExtensions.cs @@ -0,0 +1,83 @@ +using Insight.Infrastructure.Models; +using Microsoft.AspNetCore.Http; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; + +namespace Insight.Infrastructure +{ + public static class MongoCollectionExtensions + { + private const int _maximumLimit = 100; + + public static async Task> GetPagedAsync( + this IMongoCollection collection, + FilterDefinition? filter = null, + SortDefinition? sort = null, + int offset = 0, + int limit = 10, + CancellationToken cancellationToken = default) + { + if (limit > _maximumLimit) throw new InvalidOperationException("invalid limit value > 100"); + + var query = collection.Find(filter ?? Builders.Filter.Empty); + + if (sort is not null) query = query.Sort(sort); + + var data = await query.Skip(offset).Limit(limit).ToListAsync(cancellationToken).ConfigureAwait(false); + var total = await collection.EstimatedDocumentCountAsync(null, cancellationToken).ConfigureAwait(false); + + return new PagedList(data, offset, limit, total); + } + + public static async Task> GetPagedAsync( + this IMongoCollection collection, + HttpRequest request, + HttpResponse response, + FilterDefinition? filter = null, + SortDefinition? sort = null, + int offset = 0, + int limit = 10, + CancellationToken cancellationToken = default) + { + var result = await GetPagedAsync(collection, filter, sort, offset, limit, cancellationToken).ConfigureAwait(false); + + request?.AddPagination(result); + response?.AddPagination(result); + + return result; + } + + public static async Task> GetPagedAsync( + this IMongoCollection collection, + IAggregateFluent query, + int offset = 0, + int limit = 10, + CancellationToken cancellationToken = default) + { + if (limit > _maximumLimit) throw new InvalidOperationException("invalid limit value"); + + var data = await query.Skip(offset).Limit(limit).ToListAsync(cancellationToken).ConfigureAwait(false); + var total = await collection.EstimatedDocumentCountAsync(null, cancellationToken).ConfigureAwait(false); + + return new PagedList(data.Select(x => BsonSerializer.Deserialize(x)), offset, limit, total); + } + + public static async Task> GetPagedAsync( + this IMongoCollection collection, + HttpRequest request, + HttpResponse response, + IAggregateFluent query, + int offset = 0, + int limit = 10, + CancellationToken cancellationToken = default) + { + var result = await GetPagedAsync(collection, query, offset, limit, cancellationToken).ConfigureAwait(false); + + request?.AddPagination(result); + response?.AddPagination(result); + + return result; + } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Extensions/MongoDatabaseExtensions.cs b/src/Core/Insight.Infrastructure/Extensions/MongoDatabaseExtensions.cs new file mode 100644 index 0000000..3769e1d --- /dev/null +++ b/src/Core/Insight.Infrastructure/Extensions/MongoDatabaseExtensions.cs @@ -0,0 +1,57 @@ +using Insight.Infrastructure.Entities; +using MongoDB.Driver; + +namespace Insight.Infrastructure +{ + public static class MongoDatabaseExtensions + { + // internal users (roles), groups... + public static IMongoCollection User(this IMongoDatabase database) => database.GetCollection("user"); + public static IMongoCollection UserLog(this IMongoDatabase database) => database.GetCollection("user_log"); + public static IMongoCollection UserPreference(this IMongoDatabase database) => database.GetCollection("user_pref"); + public static IMongoCollection Role(this IMongoDatabase database) => database.GetCollection("role"); + + // customers + public static IMongoCollection Customer(this IMongoDatabase database) => database.GetCollection("customer"); + + // agents + public static IMongoCollection Agent(this IMongoDatabase database) => database.GetCollection("agent"); + public static IMongoCollection AgentLog(this IMongoDatabase database) => database.GetCollection("agent_log"); + + // host groups + public static IMongoCollection HostGroup(this IMongoDatabase database) => database.GetCollection("host"); + + // hosts + public static IMongoCollection Host(this IMongoDatabase database) => database.GetCollection("host"); + public static IMongoCollection HostLog(this IMongoDatabase database) => database.GetCollection("host_log"); + + // hosts extensions + public static IMongoCollection HostLogMonitoring(this IMongoDatabase database) => database.GetCollection("host_log_mon"); + public static IMongoCollection HostApplication(this IMongoDatabase database) => database.GetCollection("host_app"); + public static IMongoCollection HostDrive(this IMongoDatabase database) => database.GetCollection("host_drv"); + public static IMongoCollection HostVolume(this IMongoDatabase database) => database.GetCollection("host_vol"); + public static IMongoCollection HostOs(this IMongoDatabase database) => database.GetCollection("host_os"); + public static IMongoCollection HostUpdate(this IMongoDatabase database) => database.GetCollection("host_upd"); + public static IMongoCollection HostSession(this IMongoDatabase database) => database.GetCollection("host_session"); + public static IMongoCollection HostService(this IMongoDatabase database) => database.GetCollection("host_svc"); + public static IMongoCollection HostPrinter(this IMongoDatabase database) => database.GetCollection("host_prn"); + public static IMongoCollection HostMainboard(this IMongoDatabase database) => database.GetCollection("host_board"); + public static IMongoCollection HostProcessor(this IMongoDatabase database) => database.GetCollection("host_cpu"); + public static IMongoCollection HostMemory(this IMongoDatabase database) => database.GetCollection("host_mem"); + public static IMongoCollection HostVideocard(this IMongoDatabase database) => database.GetCollection("host_gpu"); + public static IMongoCollection HostSystemUser(this IMongoDatabase database) => database.GetCollection("host_sysusr"); + public static IMongoCollection HostSystemGroup(this IMongoDatabase database) => database.GetCollection("host_sysgrp"); + public static IMongoCollection HostSystemUserSystemGroup(this IMongoDatabase database) => database.GetCollection("host_sysusr_sysgrp"); + public static IMongoCollection HostSystem(this IMongoDatabase database) => database.GetCollection("host_sys"); + public static IMongoCollection HostStoragePool(this IMongoDatabase database) => database.GetCollection("host_sp"); + public static IMongoCollection HostStoragePoolPhysicalDisk(this IMongoDatabase database) => database.GetCollection("host_sp.pd"); + public static IMongoCollection HostStoragePoolVirtualDisk(this IMongoDatabase database) => database.GetCollection("host_sp.vd"); + public static IMongoCollection HostHypervisorVirtualMaschine(this IMongoDatabase database) => database.GetCollection("host_hv_vm"); + public static IMongoCollection HostVirtualMaschineConfig(this IMongoDatabase database) => database.GetCollection("host_hv_vm_cfg"); + public static IMongoCollection HostInterface(this IMongoDatabase database) => database.GetCollection("host_if"); + public static IMongoCollection HostInterfaceAddress(this IMongoDatabase database) => database.GetCollection("host_if_addr"); + public static IMongoCollection HostInterfaceGateway(this IMongoDatabase database) => database.GetCollection("host_if_gw"); + public static IMongoCollection HostInterfaceNameserver(this IMongoDatabase database) => database.GetCollection("host_if_ns"); + public static IMongoCollection HostInterfaceRoute(this IMongoDatabase database) => database.GetCollection("host_if_rt"); + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Extensions/ServiceExtensions.cs b/src/Core/Insight.Infrastructure/Extensions/ServiceExtensions.cs new file mode 100644 index 0000000..e8e49d6 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Extensions/ServiceExtensions.cs @@ -0,0 +1,263 @@ +using AspNetCore.Identity.MongoDbCore.Extensions; +using AspNetCore.Identity.MongoDbCore.Infrastructure; +using Insight.Infrastructure.Entities; +using Insight.Infrastructure.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.Core.Configuration; +using System.Text; + +namespace Insight.Infrastructure +{ + public static class ServiceExtensions + { + public static IServiceCollection AddDatabase(this IServiceCollection services, IConfiguration configuration, ILoggerFactory? loggerFactory = null) + { + var connectionString = configuration.GetValue(Appsettings.Database) ?? throw new Exception($"{Appsettings.Database} value not set (appsettings)"); + + var settings = MongoClientSettings.FromUrl(new MongoUrl(connectionString)); + settings.ConnectTimeout = TimeSpan.FromSeconds(3); + settings.IPv6 = false; + + if (loggerFactory is not null) + { + settings.LoggingSettings = new LoggingSettings(loggerFactory); + } + + services.AddSingleton(new MongoClient(settings)); + services.AddSingleton(provider => provider.GetRequiredService()); + return services.AddSingleton(provider => provider.GetRequiredService().GetDatabase(Settings.Database)); + } + + public static IServiceCollection AddInfrastructureServices(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + return services; + } + + public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration configuration) + { + var connectionString = configuration.GetValue(Appsettings.Database) ?? throw new Exception($"{Appsettings.Database} value not set (appsettings)"); + + services.AddIdentity(options => + { + + }) + .AddMongoDbStores(connectionString, Settings.Database) + .AddDefaultTokenProviders() + .AddSignInManager(); + + return services; + } + + public static IServiceCollection AddTokenServices(this IServiceCollection services, IConfiguration configuration) + { + var options = new Models.TokenOptions( + key: configuration.GetValue(Appsettings.JwtKey) ?? throw new Exception($"{Appsettings.JwtKey} value not set (appsettings)"), + expires: configuration.GetValue(Appsettings.JwtExp) ?? throw new Exception($"{Appsettings.JwtExp} value not set (appsettings)"), + audience: configuration.GetValue(Appsettings.JwtAudience) ?? throw new Exception($"{Appsettings.JwtAudience} value not set (appsettings)"), + issuer: configuration.GetValue(Appsettings.JwtIssuer) ?? throw new Exception($"{Appsettings.JwtIssuer} value not set (appsettings)")); + + services.AddSingleton(options); + services.AddTransient(); + + return services; + } + + public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) + { + // REWRITE TO COOKIE ONLY FOR WEB + + services.AddAuthentication(options => + { + options.DefaultScheme = "Custom"; + options.DefaultChallengeScheme = "Custom"; + }) + .AddCookie("Cookies", options => + { + //options.Cookie.Domain = "insight.webmatic.de"; + options.Cookie.Name = "insight"; + options.LoginPath = "/account/login"; + options.LogoutPath = "/account/logout"; + options.ExpireTimeSpan = TimeSpan.FromHours(1); + options.SlidingExpiration = true; + + options.Events.OnRedirectToLogin = options => + { + if (options.Request.Path.StartsWithSegments("/api") && options.Response.StatusCode == 200) + options.Response.StatusCode = 401; + else + options.Response.Redirect(options.RedirectUri); + + return Task.CompletedTask; + }; + }) + .AddJwtBearer("Bearer", options => + { + options.RequireHttpsMetadata = false; + options.SaveToken = true; + + options.TokenValidationParameters.ValidateActor = false; + + options.TokenValidationParameters.ValidAudience = configuration.GetSection("Jwt:Audience").Value; + options.TokenValidationParameters.ValidateAudience = true; + + options.TokenValidationParameters.ValidIssuer = configuration.GetSection("Jwt:Issuer").Value; + options.TokenValidationParameters.ValidateIssuer = true; + + options.TokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(configuration.GetSection("Jwt:Key").Value ?? throw new ArgumentNullException(nameof(TokenValidationParameters), "Jwt:Key")) + ); + + options.TokenValidationParameters.ValidateIssuerSigningKey = true; + options.TokenValidationParameters.ValidateLifetime = true; + }) + .AddPolicyScheme("Custom", "Custom", options => + { + options.ForwardDefaultSelector = context => + { + string authorization = context.Request.Headers[HeaderNames.Authorization]; + + if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer ")) return "Bearer"; + + return "Cookies"; + }; + }); + + return services; + } + + public static IServiceCollection AddBearerAuthentication(this IServiceCollection services, IConfiguration configuration) + { + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.RequireHttpsMetadata = false; + options.SaveToken = true; + + options.TokenValidationParameters.ValidateActor = false; + + options.TokenValidationParameters.ValidAudience = configuration.GetValue(Appsettings.JwtAudience) ?? throw new Exception($"{Appsettings.JwtAudience} value not set (appsettings)"); + options.TokenValidationParameters.ValidateAudience = true; + + options.TokenValidationParameters.ValidIssuer = configuration.GetValue(Appsettings.JwtIssuer) ?? throw new Exception($"{Appsettings.JwtIssuer} value not set (appsettings)"); + options.TokenValidationParameters.ValidateIssuer = true; + + options.TokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(configuration.GetValue(Appsettings.JwtKey) ?? throw new Exception($"{Appsettings.JwtKey} value not set (appsettings)")) + ); + + options.TokenValidationParameters.ValidateIssuerSigningKey = true; + options.TokenValidationParameters.ValidateLifetime = true; + }); + + return services; + } + + public static IServiceCollection AddProxyServices(this IServiceCollection services, IConfiguration configuration) + { + // add before routing + services.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + }); + + return services; + } + + public static IServiceCollection AddRoutingServices(this IServiceCollection services, IConfiguration configuration) + { + // add after proxy + services.AddRouting(options => + { + options.LowercaseUrls = true; + }); + + return services; + } + + private static IServiceCollection AddIdentityServices2(this IServiceCollection services, IConfiguration configuration) + { + var identityOptions = new MongoDbIdentityConfiguration + { + MongoDbSettings = new MongoDbSettings + { + ConnectionString = configuration.GetSection("ConnectionStrings:Mongo").Value, + DatabaseName = "insight" + }, + IdentityOptionsAction = options => + { + options.User.RequireUniqueEmail = true; + options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@.-_"; + + options.Password.RequireDigit = false; + options.Password.RequiredLength = 8; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + options.Password.RequireLowercase = false; + + options.SignIn.RequireConfirmedAccount = false; + options.SignIn.RequireConfirmedEmail = false; + options.SignIn.RequireConfirmedPhoneNumber = false; + + options.Lockout.MaxFailedAccessAttempts = 5; + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); + } + }; + + services.ConfigureMongoDbIdentity(identityOptions) + .AddDefaultTokenProviders() + .AddSignInManager(); + + return services; + } + + private static IServiceCollection AddIdentityAuthentication(this IServiceCollection services, IConfiguration configuration) + { + services.AddAuthentication(options => + { + //options.DefaultAuthenticateScheme = + }); + //cookieBuilder.ApplicationCookie = builder.AddApplicationCookie(); + //cookieBuilder.ExternalCookie = builder.AddExternalCookie(); + //cookieBuilder.TwoFactorRememberMeCookie = builder.AddTwoFactorRememberMeCookie(); + //cookieBuilder.TwoFactorUserIdCookie = builder.AddTwoFactorUserIdCookie(); + //.AddCookie(options => + //{ + // options. + //}; + //.AddIdentityCookies(); + //.AddCookie(options => + //{ + // // Specify where to redirect un-authenticated users + // options.LoginPath = "/account/login"; + + // // Specify the name of the auth cookie. + // // ASP.NET picks a dumb name by default. + // options.Cookie.Name = "insight"; + //}); + + return services; + } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Insight.Infrastructure.csproj b/src/Core/Insight.Infrastructure/Insight.Infrastructure.csproj new file mode 100644 index 0000000..d4cec7c --- /dev/null +++ b/src/Core/Insight.Infrastructure/Insight.Infrastructure.csproj @@ -0,0 +1,31 @@ + + + + net7.0 + Insight.Infrastructure + Insight + 2023.7.12.0 + true + enable + + + + none + + + + none + + + + + + + + + + + + + + diff --git a/src/Core/Insight.Infrastructure/Models/Pagination.cs b/src/Core/Insight.Infrastructure/Models/Pagination.cs new file mode 100644 index 0000000..1ddc63a --- /dev/null +++ b/src/Core/Insight.Infrastructure/Models/Pagination.cs @@ -0,0 +1,49 @@ +using System.Text.Json.Serialization; + +namespace Insight.Infrastructure.Models +{ + public class PagedList + { + public PagedMetaData Meta { get; } = new(); + public IEnumerable Data { get; } + + public PagedList(IEnumerable data, int offset, int limit, long total) + { + Data = data; + Meta = new() + { + Offset = offset, + Limit = limit, + Count = data?.Count() ?? 0, + Total = total, + }; + } + } + + public class PagedDataRequest + { + [JsonPropertyName("offset")] + public int Offset { get; set; } = 0; + + [JsonPropertyName("limit")] + public int Limit { get; set; } = 10; + } + + public class PagedHeaderData : PagedDataRequest + { + [JsonPropertyName("count")] + public int Count { get; set; } = 0; + + [JsonPropertyName("total")] + public long Total { get; set; } = 0; + } + + public class PagedMetaData : PagedHeaderData + { + [JsonPropertyName("next")] + public string? Next { get; set; } + + [JsonPropertyName("previous")] + public string? Previous { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Models/TokenOptions.cs b/src/Core/Insight.Infrastructure/Models/TokenOptions.cs new file mode 100644 index 0000000..d993465 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Models/TokenOptions.cs @@ -0,0 +1,18 @@ +namespace Insight.Infrastructure.Models +{ + public class TokenOptions + { + public string Key { get; set; } + public int Expires { get; set; } + public Uri? Audience { get; set; } + public Uri? Issuer { get; set; } + + public TokenOptions(string key, int expires, Uri? audience = null, Uri? issuer = null) + { + Key = key; + Expires = expires; + Audience = audience; + Issuer = issuer; + } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Services/AccountService.cs b/src/Core/Insight.Infrastructure/Services/AccountService.cs new file mode 100644 index 0000000..ece4505 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Services/AccountService.cs @@ -0,0 +1,36 @@ +using Insight.Infrastructure.Entities; +using Insight.Infrastructure.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; + +namespace Insight.Infrastructure.Services +{ + public class AccountService + { + private readonly IMongoDatabase _database; + private readonly ILogger _logger; + + public AccountService(IMongoDatabase database, ILogger logger) + { + _database = database; + _logger = logger; + } + + public Task> GetAsync( + FilterDefinition? filter = null, + SortDefinition? sort = null, + int offset = 0, + int limit = 10, + CancellationToken cancellationToken = default) => _database.User().GetPagedAsync(filter, sort, offset, limit, cancellationToken); + + public Task> GetAsync( + HttpRequest request, + HttpResponse response, + FilterDefinition? filter = null, + SortDefinition? sort = null, + int offset = 0, + int limit = 10, + CancellationToken cancellationToken = default) => _database.User().GetPagedAsync(request, response, filter, sort, offset, limit, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Services/AgentService.cs b/src/Core/Insight.Infrastructure/Services/AgentService.cs new file mode 100644 index 0000000..580336e --- /dev/null +++ b/src/Core/Insight.Infrastructure/Services/AgentService.cs @@ -0,0 +1,36 @@ +using Insight.Infrastructure.Entities; +using Insight.Infrastructure.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; + +namespace Insight.Infrastructure.Services +{ + public class AgentService + { + private readonly IMongoDatabase _database; + private readonly ILogger _logger; + + public AgentService(IMongoDatabase database, ILogger logger) + { + _database = database; + _logger = logger; + } + + public Task> GetAsync( + FilterDefinition? filter = null, + SortDefinition? sort = null, + int offset = 0, + int limit = 10, + CancellationToken cancellationToken = default) => _database.Agent().GetPagedAsync(filter, sort, offset, limit, cancellationToken); + + public Task> GetAsync( + HttpRequest request, + HttpResponse response, + FilterDefinition? filter = null, + SortDefinition? sort = null, + int offset = 0, + int limit = 10, + CancellationToken cancellationToken = default) => _database.Agent().GetPagedAsync(request, response, filter, sort, offset, limit, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Services/AuthenticatorService.cs b/src/Core/Insight.Infrastructure/Services/AuthenticatorService.cs new file mode 100644 index 0000000..65f29df --- /dev/null +++ b/src/Core/Insight.Infrastructure/Services/AuthenticatorService.cs @@ -0,0 +1,112 @@ +using Insight.Infrastructure.Entities; +using Microsoft.AspNetCore.Identity; +using MongoDB.Driver; +using System.Globalization; +using System.Text; +using System.Text.Encodings.Web; + +namespace Insight.Infrastructure.Services +{ + public class AuthenticatorService + { + private readonly IMongoDatabase _database; + private readonly UserManager _userManager; + + public AuthenticatorService(IMongoDatabase database, UserManager userManager) + { + _database = database; + _userManager = userManager; + } + + public async Task GetStatusAsync(InsightUser user) + { + return await _userManager.GetTwoFactorEnabledAsync(user).ConfigureAwait(false); + } + + public async Task GetKeyAsync(InsightUser user) + { + return await _userManager.GetAuthenticatorKeyAsync(user).ConfigureAwait(false); + } + + public async Task ResetKeyAsync(InsightUser user) + { + var result = await _userManager.ResetAuthenticatorKeyAsync(user).ConfigureAwait(false); + return result.Succeeded; + } + + public async Task VerifyAsync(InsightUser user, string code) + { + code = code.Replace(" ", string.Empty).Replace("-", string.Empty); + return await _userManager.VerifyTwoFactorTokenAsync(user, _userManager.Options.Tokens.AuthenticatorTokenProvider, code).ConfigureAwait(false); + } + + public async Task EnableAsync(InsightUser user) + { + var result = await _userManager.SetTwoFactorEnabledAsync(user, true).ConfigureAwait(false); + return result.Succeeded; + } + + public async Task DisableAsync(InsightUser user) + { + var result = await _userManager.SetTwoFactorEnabledAsync(user, false).ConfigureAwait(false); + return result.Succeeded; + } + + public async Task DeleteAsync(InsightUser user) + { + var result = await _userManager.SetTwoFactorEnabledAsync(user, false).ConfigureAwait(false); + if (result.Succeeded is false) return false; + + result = await _userManager.RemoveAuthenticationTokenAsync(user, "[AspNetUserStore]", "AuthenticatorKey").ConfigureAwait(false); + if (result.Succeeded is false) return false; + + return true; + } + + public async Task CountRecoveryCodesAsync(InsightUser user) + { + return await _userManager.CountRecoveryCodesAsync(user).ConfigureAwait(false); + } + + public async Task UseRecoveryCodeAsync(InsightUser user, string recoveryCode) + { + var result = await _userManager.RedeemTwoFactorRecoveryCodeAsync(user, recoveryCode).ConfigureAwait(false); + return result.Succeeded; + } + + public async Task?> ResetRecoveryCodesAsync(InsightUser user, int count = 3) + { + return await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, count).ConfigureAwait(false); + } + + public string GenerateQrCode(string email, string unformattedKey) + { + var encoder = UrlEncoder.Default; + + return string.Format(CultureInfo.InvariantCulture, + @"otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6", + encoder.Encode("Insight"), + encoder.Encode(email), + unformattedKey); + } + + public static string HumanizeKey(string unformattedKey) + { + var result = new StringBuilder(); + int currentPosition = 0; + + while (currentPosition + 4 < unformattedKey.Length) + { + result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' '); + currentPosition += 4; + } + + if (currentPosition < unformattedKey.Length) + { + result.Append(unformattedKey.AsSpan(currentPosition)); + } + + return result.ToString().ToLowerInvariant(); + } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Services/CustomerService.cs b/src/Core/Insight.Infrastructure/Services/CustomerService.cs new file mode 100644 index 0000000..c88c35b --- /dev/null +++ b/src/Core/Insight.Infrastructure/Services/CustomerService.cs @@ -0,0 +1,36 @@ +using Insight.Infrastructure.Entities; +using Insight.Infrastructure.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; + +namespace Insight.Infrastructure.Services +{ + public class CustomerService + { + private readonly IMongoDatabase _database; + private readonly ILogger _logger; + + public CustomerService(IMongoDatabase database, ILogger logger) + { + _database = database; + _logger = logger; + } + + public Task> GetAsync( + FilterDefinition? filter = null, + SortDefinition? sort = null, + int offset = 0, + int limit = 10, + CancellationToken cancellationToken = default) => _database.Customer().GetPagedAsync(filter, sort, offset, limit, cancellationToken); + + public Task> GetAsync( + HttpRequest request, + HttpResponse response, + FilterDefinition? filter = null, + SortDefinition? sort = null, + int offset = 0, + int limit = 10, + CancellationToken cancellationToken = default) => _database.Customer().GetPagedAsync(request, response, filter, sort, offset, limit, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Services/HostService.cs b/src/Core/Insight.Infrastructure/Services/HostService.cs new file mode 100644 index 0000000..d39cfc9 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Services/HostService.cs @@ -0,0 +1,36 @@ +using Insight.Infrastructure.Entities; +using Insight.Infrastructure.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; + +namespace Insight.Infrastructure.Services +{ + public class HostService + { + private readonly IMongoDatabase _database; + private readonly ILogger _logger; + + public HostService(IMongoDatabase database, ILogger logger) + { + _database = database; + _logger = logger; + } + + public Task> GetAsync( + FilterDefinition? filter = null, + SortDefinition? sort = null, + int offset = 0, + int limit = 10, + CancellationToken cancellationToken = default) => _database.Host().GetPagedAsync(filter, sort, offset, limit, cancellationToken); + + public Task> GetAsync( + HttpRequest request, + HttpResponse response, + FilterDefinition? filter = null, + SortDefinition? sort = null, + int offset = 0, + int limit = 10, + CancellationToken cancellationToken = default) => _database.Host().GetPagedAsync(request, response, filter, sort, offset, limit, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Services/IdentityService.cs b/src/Core/Insight.Infrastructure/Services/IdentityService.cs new file mode 100644 index 0000000..03c7ca8 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Services/IdentityService.cs @@ -0,0 +1,138 @@ +using Insight.Infrastructure.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using System.Security.Claims; + +namespace Insight.Infrastructure.Services +{ + public class IdentityService + { + private readonly UserManager _userManager; + private readonly RoleManager _roleManager; + private readonly ILogger _logger; + + public IdentityService(UserManager userManager, RoleManager roleManager, ILogger logger) + { + _userManager = userManager; + _roleManager = roleManager; + _logger = logger; + } + + public async Task SeedAsync() + { + // SEED ROLES + if (await _roleManager.FindByNameAsync("system") is not InsightRole systemRole) + { + var result = await CreateRoleAsync("system"); + if (result.Succeeded is false) throw new InvalidProgramException("seeding: system role failed"); + + systemRole = await _roleManager.FindByNameAsync("system") ?? throw new InvalidProgramException("seeding: system role failed"); + } + + if (await _roleManager.FindByNameAsync("administrator") is not InsightRole administratorRole) + { + var result = await CreateRoleAsync("administrator"); + if (result.Succeeded is false) throw new InvalidProgramException("seeding: administrator role failed"); + + administratorRole = await _roleManager.FindByNameAsync("administrator") ?? throw new InvalidProgramException("seeding: administrator role failed"); + } + + if (await _roleManager.FindByNameAsync("chat") is not InsightRole chatRole) + { + var result = await CreateRoleAsync("chat"); + if (result.Succeeded is false) throw new InvalidProgramException("seeding: chat role failed"); + + chatRole = await _roleManager.FindByNameAsync("chat") ?? throw new InvalidProgramException("seeding: chat role failed"); + } + + // SEED USERS + if (await _userManager.FindByEmailAsync("system@insight.local") is not InsightUser systemUser) + { + var result = await CreateUserAsync("system@insight.local", "Replica3-Unroasted-Respect"); + if (result.Succeeded is false) throw new InvalidProgramException("seeding: system user failed"); + + systemUser = await _userManager.FindByEmailAsync("system@insight.local") ?? throw new InvalidProgramException("seeding: system user failed"); + } + + if (systemUser.Roles.Any(p => p == systemRole.Id) is false) + { + var assign = await _userManager.AddToRoleAsync(systemUser, systemRole.Name); + if (assign.Succeeded is false) throw new InvalidProgramException("seeding: system user roles failed"); + } + } + + public async Task CreateUserAsync(string email, string password) + { + var user = new InsightUser + { + UserName = email, + NormalizedUserName = email.ToUpperInvariant(), + Email = email, + NormalizedEmail = email.ToUpperInvariant(), + }; + + return await _userManager.CreateAsync(user, password); + } + + public async Task CreateRoleAsync(string name) + { + var role = new InsightRole + { + Name = name, + NormalizedName = name.ToUpperInvariant() + }; + + return await _roleManager.CreateAsync(role); + } + + public async Task LoginAsync(string email, string password, string? code = null) + { + if (await _userManager.FindByEmailAsync(email) is not InsightUser user) throw new InvalidDataException("Invalid Credentials"); + if (await _userManager.CheckPasswordAsync(user, password) is false) throw new InvalidDataException("Invalid Credentials"); + + if (await _userManager.GetTwoFactorEnabledAsync(user)) + { + if (string.IsNullOrWhiteSpace(code)) throw new InvalidOperationException("Requires 2FA Code"); + + var authCode = code.Replace(" ", string.Empty).Replace("-", string.Empty); + + if (await _userManager.VerifyTwoFactorTokenAsync(user, _userManager.Options.Tokens.AuthenticatorTokenProvider, authCode) is false) + { + throw new InvalidDataException("Invalid 2FA Code"); + } + } + + return user; + } + + public async Task ChangePasswordAsync(InsightUser user, string current, string @new) + { + var result = await _userManager.ChangePasswordAsync(user, current, @new).ConfigureAwait(false); + return result.Succeeded; + } + + public async Task GetByEmailAsync(string key) + { + var result = await _userManager.FindByEmailAsync(key).ConfigureAwait(false); + if (result is not null) return result; + + return null; + } + + public async Task> GetClaimsAsync(InsightUser user, bool includeRoles = true) + { + var claims = await _userManager.GetClaimsAsync(user).ConfigureAwait(false); + + if (includeRoles) + { + var roles = await _userManager.GetRolesAsync(user).ConfigureAwait(false); + foreach (var role in roles) claims.Add(new Claim(ClaimTypes.Role, role)); + } + + claims.Add(new Claim(ClaimTypes.Email, user.Email)); + claims.Add(new Claim("user", user.UserName)); + + return claims; + } + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Services/InventoryService.cs b/src/Core/Insight.Infrastructure/Services/InventoryService.cs new file mode 100644 index 0000000..c5e1608 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Services/InventoryService.cs @@ -0,0 +1,36 @@ +using Insight.Infrastructure.Entities; +using Insight.Infrastructure.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; + +namespace Insight.Infrastructure.Services +{ + public class InventoryService + { + private readonly IMongoDatabase _database; + private readonly ILogger _logger; + + public InventoryService(IMongoDatabase database, ILogger logger) + { + _database = database; + _logger = logger; + } + + public Task> GetAsync( + FilterDefinition? filter = null, + SortDefinition? sort = null, + int offset = 0, + int limit = 10, + CancellationToken cancellationToken = default) => _database.HostApplication().GetPagedAsync(filter, sort, offset, limit, cancellationToken); + + public Task> GetAsync( + HttpRequest request, + HttpResponse response, + FilterDefinition? filter = null, + SortDefinition? sort = null, + int offset = 0, + int limit = 10, + CancellationToken cancellationToken = default) => _database.HostApplication().GetPagedAsync(request, response, filter, sort, offset, limit, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Core/Insight.Infrastructure/Services/TokenService.cs b/src/Core/Insight.Infrastructure/Services/TokenService.cs new file mode 100644 index 0000000..11c0ff3 --- /dev/null +++ b/src/Core/Insight.Infrastructure/Services/TokenService.cs @@ -0,0 +1,149 @@ +using Insight.Domain.Models; +using Insight.Infrastructure.Entities; +using Insight.Infrastructure.Models; +using Microsoft.IdentityModel.Tokens; +using MongoDB.Driver; +using System.IdentityModel.Tokens.Jwt; +using System.Net; +using System.Security.Cryptography; +using System.Text; + +namespace Insight.Infrastructure.Services +{ + public class TokenService + { + private readonly TokenOptions _options; + private readonly IdentityService _identityService; + private readonly IMongoDatabase _database; + + public TokenService(TokenOptions options, IdentityService identityService, IMongoDatabase database) + { + _options = options; + _identityService = identityService; + _database = database; + } + + public async Task GetAsync(string email, string password, string? code = null, IPAddress? ipa = null) + { + var user = await _identityService.LoginAsync(email, password, code).ConfigureAwait(false); + + var accessToken = await CreateAccessTokenAsync(user, ipa).ConfigureAwait(false); + var refreshToken = await CreateRefreshTokenAsync(user, ipa).ConfigureAwait(false); + + return new TokenResponse + { + AccessToken = accessToken.Item1, + ExpireInSeconds = accessToken.Item2, + RefreshToken = refreshToken.Item1 + }; + } + + public async Task RefreshAsync(string refreshToken, IPAddress? ipa = null) + { + if (string.IsNullOrWhiteSpace(refreshToken)) throw new ArgumentNullException(nameof(refreshToken)); + + var user = await _database.User().Find(p => p.RefreshTokens.Any(t => t.Token == refreshToken)).FirstOrDefaultAsync(); + if (user is null || user.RefreshTokens is null) throw new InvalidDataException("Invalid Refresh Token"); + + var token = user.RefreshTokens.First(p => p.Token == refreshToken); + + if (token.IsRevoked) + { + // todo: revoke all descendant tokens in case this token has been compromised + throw new InvalidDataException("Invalid Refresh Token"); + } + + if (token.IsActive is false) + { + throw new InvalidDataException("Invalid Refresh Token"); + } + + // remove actual refresh token + user.RefreshTokens.Remove(token); + + // remove old refresh tokens from user + user.RefreshTokens.RemoveAll(p => p.IsExpired && p.IsRevoked is false); + + // update users refreshTokens + await _database.User().UpdateOneAsync(Builders + .Filter.Eq(p => p.UserName, user.UserName), Builders + .Update.Set(p => p.RefreshTokens, user.RefreshTokens)); + + // create new refresh token + var newRefreshToken = await CreateRefreshTokenAsync(user, ipa).ConfigureAwait(false); + + // create access token + var accessToken = await CreateAccessTokenAsync(user, ipa).ConfigureAwait(false); + + return new TokenResponse + { + AccessToken = accessToken.Item1, + ExpireInSeconds = accessToken.Item2, + RefreshToken = newRefreshToken.Item1, + }; + } + + public async Task RevokeAsync(string refreshToken, string reason, IPAddress? ipa = null) + { + if (string.IsNullOrWhiteSpace(refreshToken)) throw new ArgumentNullException(nameof(refreshToken)); + + var user = await _database.User().Find(p => p.RefreshTokens.Any(t => t.Token == refreshToken)).FirstOrDefaultAsync(); + if (user is null || user.RefreshTokens is null) throw new InvalidDataException("Invalid Refresh Token"); + + var token = user.RefreshTokens.First(p => p.Token == refreshToken); + + if (token.IsActive is false) + { + throw new InvalidDataException("Invalid Refresh Token"); + } + + token.Revoked = DateTime.Now; + token.RevokedByIp = ipa?.ToString(); + token.ReasonRevoked = reason; + } + + private async Task<(string, int)> CreateAccessTokenAsync(InsightUser user, IPAddress? ipa = null) + { + var claims = await _identityService.GetClaimsAsync(user).ConfigureAwait(false); + + var key = Encoding.UTF8.GetBytes(_options.Key); + var secret = new SymmetricSecurityKey(key); + var signing = new SigningCredentials(secret, SecurityAlgorithms.HmacSha256); + + var securityToken = new JwtSecurityToken( + _options.Issuer?.ToString(), + _options.Audience?.ToString(), + claims, + DateTime.Now, + DateTime.Now.AddSeconds(Convert.ToDouble(_options.Expires)), + signing); + + var token = new JwtSecurityTokenHandler().WriteToken(securityToken); + + return (token, (int)TimeSpan.FromMinutes(Convert.ToDouble(_options.Expires)).TotalSeconds); + } + + private async Task<(string, int)> CreateRefreshTokenAsync(InsightUser user, IPAddress? ipa = null) + { + var randomNumber = new byte[32]; + + using var rng = RandomNumberGenerator.Create(); + rng.GetBytes(randomNumber); + + var refreshToken = Convert.ToBase64String(randomNumber); + + await _database.User() + .UpdateOneAsync(Builders + .Filter.Eq(p => p.UserName, user.UserName), Builders + .Update.AddToSet(p => p.RefreshTokens, new RefreshToken + { + Token = refreshToken, + Created = DateTime.Now, + Expires = DateTime.Now.AddMinutes(30), // change offset to config based + CreatedByIp = ipa?.ToString() + })); + + return (refreshToken, (int)TimeSpan.FromMinutes(30).TotalSeconds); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Constants/Appsettings.cs b/src/Server/Insight.Server/Constants/Appsettings.cs new file mode 100644 index 0000000..29f0dd7 --- /dev/null +++ b/src/Server/Insight.Server/Constants/Appsettings.cs @@ -0,0 +1,14 @@ +namespace Insight.Server +{ + internal static class Appsettings + { + internal const string AgentServerPort = "agent.server.port"; + internal const string AgentServerCertificate = "agent.server.certificate"; + internal const string AgentServerCertificatePassword = "agent.server.certificate.password"; + internal const string DispatchWebmatic = "dispatch.webmatic"; + + internal const string WebServerPort = "web.server.port"; + internal const string WebServerCertificate = "web.server.certificate"; + internal const string WebServerCertificatePassword = "web.server.certificate.password"; + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Extensions/Async.cs b/src/Server/Insight.Server/Extensions/Async.cs new file mode 100644 index 0000000..2b41b74 --- /dev/null +++ b/src/Server/Insight.Server/Extensions/Async.cs @@ -0,0 +1,53 @@ +using System.Threading.Tasks.Dataflow; + +namespace Insight.Server.Extensions +{ + public static class Async + { + public static async Task ParallelForEach( + this IAsyncEnumerable source, + Func body, + int maxDegreeOfParallelism = DataflowBlockOptions.Unbounded, + TaskScheduler scheduler = null) + { + var options = new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = maxDegreeOfParallelism + }; + + if (scheduler != null) + options.TaskScheduler = scheduler; + + var block = new ActionBlock(body, options); + + await foreach (var item in source) + block.Post(item); + + block.Complete(); + await block.Completion; + } + + public static async Task ParallelForEach( + this IEnumerable source, + Func body, + int maxDegreeOfParallelism = DataflowBlockOptions.Unbounded, + TaskScheduler scheduler = null) + { + var options = new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = maxDegreeOfParallelism + }; + + if (scheduler != null) + options.TaskScheduler = scheduler; + + var block = new ActionBlock(body, options); + + foreach (var item in source) + block.Post(item); + + block.Complete(); + await block.Completion; + } + } +} diff --git a/src/Server/Insight.Server/Extensions/ConfigurationExtensions.cs b/src/Server/Insight.Server/Extensions/ConfigurationExtensions.cs new file mode 100644 index 0000000..6f0abe0 --- /dev/null +++ b/src/Server/Insight.Server/Extensions/ConfigurationExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Configuration; + +namespace Insight.Server.Extensions +{ + public static class ConfigurationExtensions + { + public static IConfigurationBuilder Defaults(this IConfigurationBuilder configuration) + { + configuration.Sources.Clear(); + configuration.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); + return configuration.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", optional: true, reloadOnChange: true); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Insight.Server.csproj b/src/Server/Insight.Server/Insight.Server.csproj new file mode 100644 index 0000000..3fde5e7 --- /dev/null +++ b/src/Server/Insight.Server/Insight.Server.csproj @@ -0,0 +1,71 @@ + + + + Exe + net7.0 + Insight + server + 2023.9.14.0 + Insight.Server + enable + enable + + + + none + + + + none + + + + + + + + + + Always + true + PreserveNewest + + + Always + true + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + true + Never + + + + diff --git a/src/Server/Insight.Server/Models/MonitorMessage.cs b/src/Server/Insight.Server/Models/MonitorMessage.cs new file mode 100644 index 0000000..e3eef9a --- /dev/null +++ b/src/Server/Insight.Server/Models/MonitorMessage.cs @@ -0,0 +1,27 @@ +using Insight.Agent.Enums; + +namespace Insight.Server.Models +{ + internal class MonitorMessage + { + public DateTime? Timestamp { get; set; } + public string? Community { get; set; } + public ApplicationEnum? Application { get; set; } + public CategoryEnum? Category { get; set; } + public StatusEnum? Status { get; set; } + public string? Endpoint { get; set; } + public string? Hostname { get; set; } + public string? Subject { get; set; } + public string? Message { get; set; } + + public enum ApplicationEnum + { + Unknown = 0, + Insight = 1, + Acronis = 2, + Veeam = 3, + QNAP = 4, + FreeNas = 5 + } + } +} diff --git a/src/Server/Insight.Server/Network/AgentSession.cs b/src/Server/Insight.Server/Network/AgentSession.cs new file mode 100644 index 0000000..caa907a --- /dev/null +++ b/src/Server/Insight.Server/Network/AgentSession.cs @@ -0,0 +1,89 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Server.Network.Handlers.Agent; +using Microsoft.Extensions.Logging; +using Vaitr.Network; + +namespace Insight.Server.Network +{ + public class AgentSession : TcpSession + { + public string? Id { get; set; } + + private readonly AgentHandler _agentHandler; + private readonly IEnumerable> _handlers; + + public AgentSession(AgentHandler agentHandler, IEnumerable> handlers, ISerializer serializer, ILogger logger) : base(serializer, logger) + { + _agentHandler = agentHandler; + _handlers = handlers; + } + + protected override async ValueTask OnConnectedAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Agent ({ep?}) connected", RemoteEndPoint); + + var request = new AuthenticationRequest(); + + foreach (var handler in _handlers) + { + await handler.HandleAsync(this, request, cancellationToken); + } + + await _agentHandler.ConnectedAsync(this, default); + await _agentHandler.StatisticUpdateAsync(this, default); + + _logger.LogInformation("Agent ({ep?}) ID: {id}", RemoteEndPoint, Id); + } + + protected override async ValueTask OnDisconnectedAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Agent ({ep?}) disconnected", RemoteEndPoint); + + await _agentHandler.StatisticUpdateAsync(this, default); + await _agentHandler.DisconnectedAsync(this, default); + } + + protected override async ValueTask OnSentAsync(IPacketContext context, CancellationToken cancellationToken) + { + await base.OnSentAsync(context, cancellationToken); + + await _agentHandler.StatisticUpdateAsync(this, cancellationToken); + } + + protected override async ValueTask OnReceivedAsync(IPacketContext context, CancellationToken cancellationToken) + { + await base.OnReceivedAsync(context, cancellationToken); + + if (Id is null && context.Packet is not Authentication) return; + + await _agentHandler.StatisticUpdateAsync(this, cancellationToken); + + foreach (var handler in _handlers) + { + try + { + await handler.HandleAsync(this, context.Packet, cancellationToken); + } + catch (Exception ex) + { + _logger.LogWarning("Agent ({ep?}) {ex}", RemoteEndPoint, ex.ToString()); + + //await _mediator.Send(new AgentLog(new AgentLogEntity + //{ + // Category = CategoryEnum.Network.ToString(), + // Status = StatusEnum.Error.ToString(), + // Message = e.StackTrace + //}, this), cancellationToken).ConfigureAwait(false); + } + } + } + + protected override async ValueTask OnHeartbeatAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Agent ({ep?}) Heartbeat", RemoteEndPoint); + + await _agentHandler.StatisticUpdateAsync(this, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/AgentHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/AgentHandler.cs new file mode 100644 index 0000000..c7de5d8 --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/AgentHandler.cs @@ -0,0 +1,162 @@ +using Insight.Agent.Enums; +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class AgentHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + private readonly ILogger _logger; + + public AgentHandler(IMongoDatabase database, ILogger logger) + { + _database = database; + _logger = logger; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is AuthenticationRequest authenticationRequest) + { + await AuthenticationRequestAsync(sender, authenticationRequest, cancellationToken); + } + + if (message is Authentication authentication) + { + await AuthenticationAsync(sender, authentication, cancellationToken); + } + } + + private async ValueTask AuthenticationRequestAsync(AgentSession session, AuthenticationRequest message, CancellationToken cancellationToken) + { + await session.SendAsync(message, cancellationToken); + + for (int i = 0; i < 200; i++) + { + if (session.Id is not null) + { + _logger.LogInformation("Agent ({ep?}) authenticated", session.RemoteEndPoint); + return; + } + + await Task.Delay(50, cancellationToken).ConfigureAwait(false); + } + + _logger.LogError("Authentication Timeout ({ep?})", session.RemoteEndPoint); + session.Disconnect(); + } + + private async ValueTask AuthenticationAsync(AgentSession session, Authentication authentication, CancellationToken cancellationToken) + { + if (authentication is null) + { + throw new NullReferenceException($"authentication failed (empty response)"); + } + + if (authentication.Serial == default) + { + throw new InvalidDataException($"authentication failed ({nameof(authentication.Serial)})"); + } + + //if (authentication.Version == default || authentication.Version < Domain.Constants.Configuration.Version) + //{ + // throw new InvalidDataException($"authentication failed ({nameof(authentication.Version)})"); + //} + + // upsert agent + await _database.Agent().UpdateOneAsync(Builders + .Filter + .Eq(p => p.Serial, authentication.Serial.ToString()), Builders.Update + .SetOnInsert(p => p.Insert, DateTime.Now) + .SetOnInsert(p => p.Serial, authentication.Serial.ToString()) + .Set(p => p.Update, DateTime.Now) + .Set(p => p.Connected, DateTime.Now) + .Set(p => p.Version, authentication.Version) + .Set(p => p.Endpoint, session.RemoteEndPoint?.ToString()) + .Set(p => p.Hostname, authentication.Hostname), new UpdateOptions + { + IsUpsert = true + }, cancellationToken) + .ConfigureAwait(false); + + // get agent + var agentEntity = await _database.Agent() + .Find(Builders + .Filter + .Eq(p => p.Serial, authentication.Serial.ToString())) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + + // set session id + session.Id = agentEntity.Id; + } + + public async ValueTask ConnectedAsync(AgentSession session, CancellationToken cancellationToken) + { + if (session.Id is null) return; + + // insert connect log + await _database.AgentLog() + .InsertOneAsync(new AgentLogEntity + { + Insert = DateTime.Now, + Agent = session.Id, + Category = CategoryEnum.Network.ToString(), + Status = StatusEnum.Information.ToString(), + Message = $"Connected ({session.RemoteEndPoint})", + Timestamp = DateTime.Now + }, cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async ValueTask DisconnectedAsync(AgentSession session, CancellationToken cancellationToken) + { + if (session.Id is null) return; + + // insert disconnect log + await _database.AgentLog() + .InsertOneAsync(new AgentLogEntity + { + Insert = DateTime.Now, + Agent = session.Id, + Category = CategoryEnum.Network.ToString(), + Status = StatusEnum.Information.ToString(), + Message = $"Disconnected ({session.RemoteEndPoint})", + Timestamp = DateTime.Now + }, cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async ValueTask StatisticUpdateAsync(AgentSession session, CancellationToken cancellationToken) + { + if (session.Id is null) return; + + // update agents stats + await _database.Agent().UpdateOneAsync(Builders + .Filter + .Eq(p => p.Id, session.Id), Builders + .Update + .Set(p => p.Update, DateTime.Now) + .Set(p => p.Activity, session.Activity) + .Set(p => p.SentBytes, session.SentBytes) + .Set(p => p.ReceivedBytes, session.ReceivedBytes) + .Set(p => p.SentPackets, session.SentPackets) + .Set(p => p.ReceivedPackets, session.ReceivedPackets), null, cancellationToken) + .ConfigureAwait(false); + } + + public async ValueTask LogAsync(AgentSession session, AgentLogEntity log, CancellationToken cancellationToken) + { + if (session.Id is null) return; + + await _database.AgentLog() + .InsertOneAsync(log, cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/ConsoleHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/ConsoleHandler.cs new file mode 100644 index 0000000..23eb2d3 --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/ConsoleHandler.cs @@ -0,0 +1,50 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Web.Messages; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; +using Vaitr.Network; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class ConsoleHandler : IAgentMessageHandler + { + private readonly ISessionPool _webPool; + private readonly IMongoDatabase _database; + private readonly ILogger _logger; + + public ConsoleHandler( + ISessionPool webPool, + IMongoDatabase database, + ILogger logger) + { + _webPool = webPool; + _database = database; + _logger = logger; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is ConsoleQuery consoleQuery) + { + await OnConsoleQueryAsync(sender, consoleQuery, cancellationToken); + } + } + + private async ValueTask OnConsoleQueryAsync(AgentSession session, ConsoleQuery query, CancellationToken cancellationToken) + { + // check if web online + if (_webPool.FirstOrDefault().Value is not WebSession web) return; + + await web.SendAsync(new ConsoleQueryProxy + { + Id = query.Id, + HostId = query.HostId, + Query = query.Query, + Data = query.Data, + Errors = query.Errors, + HadErrors = query.HadErrors + }, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/DriveHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/DriveHandler.cs new file mode 100644 index 0000000..ec5652c --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/DriveHandler.cs @@ -0,0 +1,145 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class DriveHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public DriveHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is DriveList drives) + { + await OnDrivesAsync(sender, drives, cancellationToken); + } + } + + private async ValueTask OnDrivesAsync(AgentSession session, List drives, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var batch = ObjectId.GenerateNewId().ToString(); + var date = DateTime.Now; + + var driveBulk = new List>(); + + if (drives is not null && drives.Any()) + { + foreach (var drive in drives) + { + var driveFilter = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Index, drive.Index) + }); + + var driveUpdate = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Index, drive.Index) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + .Set(p => p.Company, drive.Manufacturer) + .Set(p => p.Name, drive.Name) + .Set(p => p.Size, drive.Size) + .Set(p => p.Type, drive.InterfaceType) + .Set(p => p.Serial, drive.SerialNumber) + .Set(p => p.Firmware, drive.FirmwareRevision) + .Set(p => p.Status, drive.Status) + .Set(p => p.Pnp, drive.PNPDeviceID); + + driveBulk.Add(new UpdateOneModel(driveFilter, driveUpdate) + { + IsUpsert = true + }); + } + } + + driveBulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var driveResult = await _database.HostDrive().BulkWriteAsync(driveBulk, cancellationToken: cancellationToken); + + // volumes + + var volumeBulk = new List>(); + + if (drives is not null && drives.Any()) + { + foreach (var drive in drives) + { + var driveId = await _database.HostDrive() + .Find(p => p.Host == hostEntity.Id && p.Index == drive.Index) + .Project(p => p.Id) + .FirstOrDefaultAsync(); + + if (drive.Volumes is not null && drive.Volumes.Any()) + { + foreach (var volume in drive.Volumes) + { + var volumeFilter = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Drive, driveId), + Builders.Filter.Eq(x => x.Index, volume.Index) + }); + + var volumeUpdate = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Drive, driveId) + .SetOnInsert(p => p.Index, volume.Index) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + .Set(p => p.Name, volume.Name) + .Set(p => p.Label, volume.Id) + .Set(p => p.Serial, volume.SerialNumber) + .Set(p => p.Size, volume.Size) + .Set(p => p.FreeSpace, volume.FreeSpace) + .Set(p => p.Type, volume.Type) + .Set(p => p.FileSystem, volume.FileSystem) + .Set(p => p.Compressed, volume.Compressed) + .Set(p => p.Bootable, volume.Bootable) + .Set(p => p.Primary, volume.PrimaryPartition) + .Set(p => p.Boot, volume.Bootable) + .Set(p => p.BlockSize, volume.BlockSize) + .Set(p => p.Blocks, volume.NumberOfBlocks) + .Set(p => p.StartingOffset, volume.StartingOffset) + .Set(p => p.Provider, volume.ProviderName); + + volumeBulk.Add(new UpdateOneModel(volumeFilter, volumeUpdate) + { + IsUpsert = true + }); + } + } + } + } + + volumeBulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var volumeResult = await _database.HostVolume().BulkWriteAsync(volumeBulk, cancellationToken: cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/EventHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/EventHandler.cs new file mode 100644 index 0000000..1bd5c22 --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/EventHandler.cs @@ -0,0 +1,266 @@ +using Insight.Agent.Enums; +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; +using static Insight.Agent.Messages.Event; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class EventHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + private readonly ILogger _logger; + + public EventHandler(IMongoDatabase database, ILogger logger) + { + _database = database; + _logger = logger; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is Event @event) + { + await OnEventAsync(sender, @event, cancellationToken); + } + } + + private async ValueTask OnEventAsync(AgentSession session, Event @event, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + if (FilterEventId(@event)) return; + + var hostLog = await InsertHostLogAsync(hostEntity, @event, cancellationToken); + + if (hostLog is null || FilterMonitoringHostLog(hostLog)) return; + + await _database.HostLogMonitoring() + .InsertOneAsync(new HostLogMonitoringEntity + { + Host = hostEntity.Id, + Insert = hostLog.Insert, + Timestamp = hostLog.Timestamp, + Category = hostLog.Category, + Status = hostLog.Status, + Message = hostLog.Message, + Dispatch = DispatchEnum.Pending.ToString() + }, cancellationToken: cancellationToken); + } + + private async ValueTask InsertAgentLogAsync(AgentSession session, Event @event, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent() + .Aggregate() + .Match(Builders.Filter.Eq(p => p.Id, session.Id)) + .Lookup(_database.Host(), p => p.Serial, p => p.Agent, p => p.Hosts) + .FirstOrDefaultAsync(cancellationToken); + + if (agentEntity is null) return null; + + StatusEnum? status = @event.Status switch + { + StatusType.Information => StatusEnum.Information, + StatusType.Warning => StatusEnum.Warning, + StatusType.Error => StatusEnum.Error, + _ => null + }; + + CategoryEnum? category = @event.Category.ToLower() switch + { + "network" => CategoryEnum.Network, + "application" => CategoryEnum.Application, + "security" => CategoryEnum.Security, + "system" => CategoryEnum.System, + _ => null + }; + + var date = DateTime.Now; + + var log = new AgentLogEntity + { + Insert = date, + Agent = agentEntity.Id, + Timestamp = @event.Timestamp, + EventId = @event.EventId.ToString(), + Source = @event.Source, + Status = status.ToString(), + Category = category.ToString(), + Message = @event.Message + }; + + await _database.AgentLog().InsertOneAsync(log, cancellationToken: cancellationToken); + return log; + } + + private async ValueTask InsertHostLogAsync(HostEntity hostEntity, Event @event, CancellationToken cancellationToken) + { + StatusEnum? status = @event.Status switch + { + StatusType.Information => StatusEnum.Information, + StatusType.Warning => StatusEnum.Warning, + StatusType.Error => StatusEnum.Error, + StatusType.Critical => StatusEnum.Error, + StatusType.Unknown => null, + _ => null + }; + + CategoryEnum? category = null; + switch (@event.Category) + { + case var _ when @event.Category.Contains("network", StringComparison.InvariantCultureIgnoreCase): + category = CategoryEnum.Network; + break; + + case var _ when @event.Category.Contains("application", StringComparison.InvariantCultureIgnoreCase): + category = CategoryEnum.Application; + break; + + case var _ when @event.Category.Contains("security", StringComparison.InvariantCultureIgnoreCase): + category = CategoryEnum.Security; + break; + + case var _ when @event.Category.Contains("system", StringComparison.InvariantCultureIgnoreCase): + category = CategoryEnum.System; + break; + + case var _ when @event.Category.Contains("printservice", StringComparison.InvariantCultureIgnoreCase): + category = CategoryEnum.Printer; + break; + + case var _ when @event.Category.Contains("taskscheduler", StringComparison.InvariantCultureIgnoreCase): + category = CategoryEnum.Task; + break; + + case var _ when @event.Category.Contains("terminalservices", StringComparison.InvariantCultureIgnoreCase): + category = CategoryEnum.RDP; + break; + + case var _ when @event.Category.Contains("smbclient", StringComparison.InvariantCultureIgnoreCase): + category = CategoryEnum.Network; + break; + + case var _ when @event.Category.Contains("smbserver", StringComparison.InvariantCultureIgnoreCase): + category = CategoryEnum.Network; + break; + + case var _ when @event.Category.Contains("storagespaces", StringComparison.InvariantCultureIgnoreCase): + category = CategoryEnum.System; + break; + + case var _ when @event.Category.Contains("diagnostics", StringComparison.InvariantCultureIgnoreCase): + category = CategoryEnum.System; + break; + + default: + break; + } + + var date = DateTime.Now; + + var log = new HostLogEntity + { + Insert = date, + Host = hostEntity.Id, + Timestamp = @event.Timestamp, + EventId = @event.EventId.ToString(), + Status = status.ToString(), + Source = @event.Source, + Category = category?.ToString(), + Message = @event.Message + }; + + await _database.HostLog().InsertOneAsync(log, cancellationToken: cancellationToken); + return log; + } + + private bool FilterEventId(Event @event) + { + var filter = new List + { + 0, + 3, + 4, + 16, + 37, + 900, + 902, + 903, + 1001, + 1003, + 1008, + 1023, + 1066, + 4624, + 4625, + 4634, + 4648, + 4672, + 4776, + 4798, + 4799, + 5058, + 5059, + 5061, + 5379, + 5612, + 5781, + //7036, + 8014, + 8016, + 8019, + 8194, + 8224, + 9009, + 9027, + 10001, + 10016, + 16384, + 16394, + 36874, + 36888, + }; + + if (filter.Any(p => p == @event.EventId)) return true; + return false; + } + + private bool FilterMonitoringHostLog(HostLogEntity hostLog) + { + //_logger.LogDebug($"try filter event: {hostLog.Category}.{hostLog.Source}.{hostLog.Status}"); + + if (Enum.TryParse(hostLog.Status, out StatusType status) is false) return true; + + if (hostLog.Category == CategoryEnum.System.ToString()) + { + if (hostLog.Source == "Service Control Manager" && status < StatusType.Warning) return true; + } + + if (hostLog.Category == CategoryEnum.Task.ToString()) + { + if (status < StatusType.Error) return true; + } + + // skip rdp infos + if (hostLog.Category == CategoryEnum.RDP.ToString()) + { + if (hostLog.EventId == "261") return true; + } + + // skip smbclient (veeam errors) + if (hostLog.Category == CategoryEnum.Security.ToString()) + { + if (hostLog.Source == "Microsoft-Windows-SMBClient") return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/InterfaceHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/InterfaceHandler.cs new file mode 100644 index 0000000..7cfc212 --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/InterfaceHandler.cs @@ -0,0 +1,299 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class InterfaceHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public InterfaceHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is InterfaceList interfaces) + { + await OnInterfacesAsync(sender, interfaces, cancellationToken); + } + } + + private async ValueTask OnInterfacesAsync(AgentSession session, List interfaces, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var batch = ObjectId.GenerateNewId().ToString(); + var date = DateTime.Now; + + // interfaces + + if (interfaces is not null && interfaces.Any()) + { + var interfaceBulk = new List>(); + + foreach (var @interface in interfaces) + { + var interfaceFilter = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Index, @interface.Index) + }); + + var interfaceUpdate = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Index, @interface.Index) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + + .Set(p => p.Mac, @interface?.Mac) + .Set(p => p.Name, @interface?.Name) + .Set(p => p.Description, @interface?.Description) + .Set(p => p.Physical, @interface?.Physical) + .Set(p => p.Status, @interface?.Status?.ToString()) + .Set(p => p.Suffix, @interface?.Suffix) + .Set(p => p.Speed, @interface?.Speed) + .Set(p => p.Ipv4Mtu, @interface?.Ipv4Mtu) + .Set(p => p.Ipv4Dhcp, @interface?.Ipv4Dhcp) + + .Set(p => p.Ipv4Forwarding, @interface?.Ipv4Forwarding) + .Set(p => p.Ipv6Mtu, @interface?.Ipv6Mtu) + .Set(p => p.Sent, @interface?.Sent) + .Set(p => p.Received, @interface?.Received) + .Set(p => p.IncomingPacketsDiscarded, @interface?.IncomingPacketsDiscarded) + .Set(p => p.IncomingPacketsWithErrors, @interface?.IncomingPacketsWithErrors) + .Set(p => p.IncomingUnknownProtocolPackets, @interface?.IncomingUnknownProtocolPackets) + .Set(p => p.OutgoingPacketsDiscarded, @interface?.OutgoingPacketsDiscarded) + .Set(p => p.OutgoingPacketsWithErrors, @interface?.OutgoingPacketsWithErrors); + + interfaceBulk.Add(new UpdateOneModel(interfaceFilter, interfaceUpdate) + { + IsUpsert = true + }); + } + + interfaceBulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var interfaceResult = await _database.HostInterface().BulkWriteAsync(interfaceBulk, cancellationToken: cancellationToken); + } + + // addresses + + if (interfaces is not null && interfaces.Any()) + { + var addressBulk = new List>(); + + foreach (var @interface in interfaces) + { + var interfaceId = await _database.HostInterface() + .Find(p => p.Host == hostEntity.Id && p.Index == @interface.Index) + .Project(p => p.Id) + .FirstOrDefaultAsync(); + + if (@interface.Addresses is not null && @interface.Addresses.Any()) + { + foreach (var address in @interface.Addresses) + { + var addressFilter = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Interface, interfaceId), + Builders.Filter.Eq(x => x.Address, address?.IpAddress?.Address), + Builders.Filter.Eq(x => x.Mask, address?.Ipv4Mask?.Address) + }); + + var addressUpdate = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Interface, interfaceId) + .SetOnInsert(p => p.Address, address?.IpAddress?.Address) + .SetOnInsert(p => p.Mask, address?.Ipv4Mask?.Address) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch); + + addressBulk.Add(new UpdateOneModel(addressFilter, addressUpdate) + { + IsUpsert = true + }); + } + } + } + + addressBulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var addressResult = await _database.HostInterfaceAddress().BulkWriteAsync(addressBulk, cancellationToken: cancellationToken); + } + + // gateways + + if (interfaces is not null && interfaces.Any()) + { + var gatewayBulk = new List>(); + + foreach (var @interface in interfaces) + { + var interfaceId = await _database.HostInterface() + .Find(p => p.Host == hostEntity.Id && p.Index == @interface.Index) + .Project(p => p.Id) + .FirstOrDefaultAsync(); + + if (@interface.Gateways is not null && @interface.Gateways.Any()) + { + foreach (var gateway in @interface.Gateways) + { + var gatewayFilter = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Interface, interfaceId), + Builders.Filter.Eq(x => x.Address, gateway?.Address) + }); + + var gatewayUpdate = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Interface, interfaceId) + .SetOnInsert(p => p.Address, gateway?.Address) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch); + + gatewayBulk.Add(new UpdateOneModel(gatewayFilter, gatewayUpdate) + { + IsUpsert = true + }); + } + } + } + + gatewayBulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var gatewayResult = await _database.HostInterfaceGateway().BulkWriteAsync(gatewayBulk, cancellationToken: cancellationToken); + } + + // nameservers + + if (interfaces is not null && interfaces.Any()) + { + var nameserverBulk = new List>(); + + foreach (var @interface in interfaces) + { + var interfaceId = await _database.HostInterface() + .Find(p => p.Host == hostEntity.Id && p.Index == @interface.Index) + .Project(p => p.Id) + .FirstOrDefaultAsync(); + + if (@interface.Dns is not null && @interface.Dns.Any()) + { + foreach (var nameserver in @interface.Dns) + { + var nameserverFilter = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Interface, interfaceId), + Builders.Filter.Eq(x => x.Address, nameserver?.Address) + }); + + var nameserverUpdate = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Interface, interfaceId) + .SetOnInsert(p => p.Address, nameserver?.Address) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch); + + nameserverBulk.Add(new UpdateOneModel(nameserverFilter, nameserverUpdate) + { + IsUpsert = true + }); + } + } + } + + nameserverBulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var nameserverResult = await _database.HostInterfaceNameserver().BulkWriteAsync(nameserverBulk, cancellationToken: cancellationToken); + } + + // routes + + if (interfaces is not null && interfaces.Any()) + { + var routeBulk = new List>(); + + foreach (var @interface in interfaces) + { + var interfaceId = await _database.HostInterface() + .Find(p => p.Host == hostEntity.Id && p.Index == @interface.Index) + .Project(p => p.Id) + .FirstOrDefaultAsync(); + + if (@interface.Routes is not null && @interface.Routes.Any()) + { + foreach (var route in @interface.Routes) + { + var routeFilter = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Interface, interfaceId), + Builders.Filter.Eq(x => x.Destination, route?.Destination?.Address), + Builders.Filter.Eq(x => x.Gateway, route?.Gateway?.Address), + Builders.Filter.Eq(x => x.Mask, route?.Mask), + }); + + var routeUpdate = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Interface, interfaceId) + .SetOnInsert(p => p.Destination, route?.Destination?.Address) + .SetOnInsert(p => p.Gateway, route?.Gateway?.Address) + .SetOnInsert(p => p.Mask, route?.Mask) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + + .Set(p => p.Metric, route?.Metric); + + routeBulk.Add(new UpdateOneModel(routeFilter, routeUpdate) + { + IsUpsert = true + }); + } + } + } + + routeBulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var routeResult = await _database.HostInterfaceRoute().BulkWriteAsync(routeBulk, cancellationToken: cancellationToken); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/MainboardHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/MainboardHandler.cs new file mode 100644 index 0000000..10e6121 --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/MainboardHandler.cs @@ -0,0 +1,51 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class MainboardHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public MainboardHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is Mainboard mainboard) + { + await OnMainboardAsync(sender, mainboard, cancellationToken); + } + } + + private async ValueTask OnMainboardAsync(AgentSession session, Mainboard mainboard, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var date = DateTime.Now; + + await _database.HostMainboard().UpdateOneAsync(p => p.Host == hostEntity.Id, Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .Set(p => p.Update, date) + .Set(p => p.Name, mainboard?.Manufacturer) + .Set(p => p.Name, mainboard?.Model) + .Set(p => p.Serial, mainboard?.Serial) + .Set(p => p.Bios, mainboard?.BiosManufacturer) + .Set(p => p.Version, mainboard?.BiosVersion) + .Set(p => p.Date, mainboard?.BiosDate), new UpdateOptions + { + IsUpsert = true + }, cancellationToken); + } + } +} diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/MemoryHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/MemoryHandler.cs new file mode 100644 index 0000000..99d78cd --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/MemoryHandler.cs @@ -0,0 +1,83 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class MemoryHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public MemoryHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is MemoryList memory) + { + await OnMemoryAsync(sender, memory, cancellationToken); + } + } + + private async ValueTask OnMemoryAsync(AgentSession session, List memory, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var batch = ObjectId.GenerateNewId().ToString(); + var date = DateTime.Now; + + var bulk = new List>(); + + if (memory is not null && memory.Any()) + { + foreach (var mem in memory) + { + var filterDefinition = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Index, mem.Index) + }); + + var updateDefinition = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Index, mem.Index) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + .Set(p => p.Company, mem.Manufacturer) + .Set(p => p.Name, mem.Model) + .Set(p => p.Tag, mem.Tag) + .Set(p => p.Location, mem.Location) + .Set(p => p.Serial, mem.Serial) + .Set(p => p.Capacity, mem.Capacity) + .Set(p => p.Clock, mem.Speed) + .Set(p => p.CurrentClock, mem.ConfiguredSpeed) + .Set(p => p.Voltage, mem.Voltage) + .Set(p => p.CurrentVoltage, mem.ConfiguredVoltage); + + bulk.Add(new UpdateOneModel(filterDefinition, updateDefinition) + { + IsUpsert = true + }); + } + } + + bulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var result = await _database.HostMemory().BulkWriteAsync(bulk, cancellationToken: cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/OperationSystemHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/OperationSystemHandler.cs new file mode 100644 index 0000000..ca8743e --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/OperationSystemHandler.cs @@ -0,0 +1,51 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class OperationSystemHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public OperationSystemHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is OperationSystem os) + { + await OnOperationSystemAsync(sender, os, cancellationToken); + } + } + + private async ValueTask OnOperationSystemAsync(AgentSession session, OperationSystem operatingSystem, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var date = DateTime.Now; + + await _database.HostOs().UpdateOneAsync(p => p.Host == hostEntity.Id, Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .Set(p => p.Update, date) + .Set(p => p.Name, operatingSystem.Name) + .Set(p => p.Version, operatingSystem.Version) + .Set(p => p.Architecture, operatingSystem.Architecture.ToString()) + .Set(p => p.SerialNumber, operatingSystem.SerialNumber) + .Set(p => p.Virtual, operatingSystem.Virtual) + .Set(p => p.Installed, operatingSystem.InstallDate), new UpdateOptions + { + IsUpsert = true + }, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/PrinterHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/PrinterHandler.cs new file mode 100644 index 0000000..eed8b2c --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/PrinterHandler.cs @@ -0,0 +1,76 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class PrinterHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public PrinterHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is PrinterList printers) + { + await OnPrintersAsync(sender, printers, cancellationToken); + } + } + + private async ValueTask OnPrintersAsync(AgentSession session, List printers, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var batch = ObjectId.GenerateNewId().ToString(); + var date = DateTime.Now; + + var bulk = new List>(); + + if (printers is not null && printers.Any()) + { + foreach (var printer in printers) + { + var filterDefinition = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Name, printer.Name) + }); + + var updateDefinition = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Name, printer.Name) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + .Set(p => p.Port, printer.Port) + .Set(p => p.Location, printer.Location) + .Set(p => p.Comment, printer.Comment); + + bulk.Add(new UpdateOneModel(filterDefinition, updateDefinition) + { + IsUpsert = true + }); + } + } + + bulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var result = await _database.HostPrinter().BulkWriteAsync(bulk, cancellationToken: cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/ProcessorHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/ProcessorHandler.cs new file mode 100644 index 0000000..a4f9c5d --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/ProcessorHandler.cs @@ -0,0 +1,87 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class ProcessorHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public ProcessorHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is ProcessorList processors) + { + await OnProcessorsAsync(sender, processors, cancellationToken); + } + } + + private async ValueTask OnProcessorsAsync(AgentSession session, List processors, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var batch = ObjectId.GenerateNewId().ToString(); + var date = DateTime.Now; + + var bulk = new List>(); + + if (processors is not null && processors.Any()) + { + foreach (var processor in processors) + { + var filterDefinition = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Index, processor.Index) + }); + + var updateDefinition = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Index, processor.Index) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + .Set(p => p.Company, processor.Manufacturer) + .Set(p => p.Name, processor.Name) + .Set(p => p.Socket, processor.Socket) + .Set(p => p.Serial, processor.SerialNumber) + .Set(p => p.Version, processor.Version) + .Set(p => p.Cores, processor.Cores) + .Set(p => p.LogicalCores, processor.LogicalCores) + .Set(p => p.Clock, processor.MaxSpeed) + .Set(p => p.CurrentClock, processor.CurrentSpeed) + .Set(p => p.L1Size, processor.L1Size) + .Set(p => p.L2Size, processor.L2Size) + .Set(p => p.L3Size, processor.L3Size) + .Set(p => p.Virtualization, processor.Virtualization) + .Set(p => p.PNP, processor.DeviceId); + + bulk.Add(new UpdateOneModel(filterDefinition, updateDefinition) + { + IsUpsert = true + }); + } + } + + bulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var result = await _database.HostProcessor().BulkWriteAsync(bulk, cancellationToken: cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/ServiceHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/ServiceHandler.cs new file mode 100644 index 0000000..1108236 --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/ServiceHandler.cs @@ -0,0 +1,81 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class ServiceHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public ServiceHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is ServiceList services) + { + await OnServicesAsync(sender, services, cancellationToken); + } + } + + private async ValueTask OnServicesAsync(AgentSession session, List services, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var batch = ObjectId.GenerateNewId().ToString(); + var date = DateTime.Now; + + var bulk = new List>(); + + if (services is not null && services.Any()) + { + foreach (var service in services) + { + var filterDefinition = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Name, service.Name) + }); + + var updateDefinition = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Name, service.Name) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + .Set(p => p.DisplayName, service.Display) + .Set(p => p.Description, service.Description) + .Set(p => p.StartMode, service.StartMode.ToString()) + .Set(p => p.State, service.Status.ToString()) + .Set(p => p.ProcessId, service.ProcessId) + .Set(p => p.Delay, service.Delay) + .Set(p => p.Path, service.PathName) + .Set(p => p.Account, service.Account); + + bulk.Add(new UpdateOneModel(filterDefinition, updateDefinition) + { + IsUpsert = true + }); + } + } + + bulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var result = await _database.HostService().BulkWriteAsync(bulk, cancellationToken: cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/SessionHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/SessionHandler.cs new file mode 100644 index 0000000..ffad33d --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/SessionHandler.cs @@ -0,0 +1,77 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class SessionHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public SessionHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is SessionList sessions) + { + await OnSessionsAsync(sender, sessions, cancellationToken); + } + } + + private async ValueTask OnSessionsAsync(AgentSession session, List sessions, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var batch = ObjectId.GenerateNewId().ToString(); + var date = DateTime.Now; + + var bulk = new List>(); + + if (sessions is not null && sessions.Any()) + { + foreach (var sess in sessions) + { + var filterDefinition = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Sid, sess.Sid) + }); + + var updateDefinition = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Sid, sess.Sid) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + .Set(p => p.User, sess.User) + .Set(p => p.Remote, sess.Remote) + .Set(p => p.Type, sess.Type) + .Set(p => p.State, sess.Status); + + bulk.Add(new UpdateOneModel(filterDefinition, updateDefinition) + { + IsUpsert = true + }); + } + } + + bulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var result = await _database.HostSession().BulkWriteAsync(bulk, cancellationToken: cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/SoftwareHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/SoftwareHandler.cs new file mode 100644 index 0000000..1e986fa --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/SoftwareHandler.cs @@ -0,0 +1,78 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class SoftwareHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public SoftwareHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is ApplicationList applications) + { + await OnApplicationsAsync(sender, applications, cancellationToken); + } + } + + private async ValueTask OnApplicationsAsync(AgentSession session, List applications, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var batch = ObjectId.GenerateNewId().ToString(); + var date = DateTime.Now; + + var bulk = new List>(); + + if (applications is not null && applications.Any()) + { + foreach (var app in applications) + { + var filterDefinition = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Name, app.Name), + Builders.Filter.Eq(x => x.Architecture, app.Architecture?.ToString()) + }); + + var updateDefinition = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Name, app.Name) + .SetOnInsert(p => p.Architecture, app.Architecture?.ToString()) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + .Set(p => p.Company, app.Publisher) + .Set(p => p.Version, app.Version) + .Set(p => p.InstallDate, app.InstallDate); + + bulk.Add(new UpdateOneModel(filterDefinition, updateDefinition) + { + IsUpsert = true + }); + } + } + + bulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var result = await _database.HostApplication().BulkWriteAsync(bulk, cancellationToken: cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/StoragePoolHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/StoragePoolHandler.cs new file mode 100644 index 0000000..7b00c9c --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/StoragePoolHandler.cs @@ -0,0 +1,253 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class StoragePoolHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public StoragePoolHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is StoragePoolList storagePools) + { + await OnStoragePoolsAsync(sender, storagePools, cancellationToken); + } + } + + private async ValueTask OnStoragePoolsAsync(AgentSession session, List? storagePools, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var batch = ObjectId.GenerateNewId().ToString(); + var date = DateTime.Now; + + // storagepools + + if (storagePools is not null && storagePools.Any()) + { + var storagepoolBulk = new List>(); + + foreach (var storagePool in storagePools) + { + var storagePoolFilter = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.UniqueId, storagePool.UniqueId) + }); + + List? states = null; + + if (storagePool.States is not null) + { + states = new List(); + + foreach (var state in storagePool.States) + { + states.Add(state.ToString()); + } + } + + var storagePoolUpdate = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.UniqueId, storagePool.UniqueId) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + + .Set(p => p.Name, storagePool.FriendlyName) + .Set(p => p.Health, storagePool.Health?.ToString()) + .Set(p => p.Resiliency, storagePool.Resiliency) + .Set(p => p.Primordial, storagePool.IsPrimordial) + .Set(p => p.ReadOnly, storagePool.IsReadOnly) + .Set(p => p.Clustered, storagePool.IsClustered) + .Set(p => p.Size, storagePool.Size) + .Set(p => p.AllocatedSize, storagePool.AllocatedSize) + .Set(p => p.SectorSize, storagePool.SectorSize) + .Set(p => p.States, states); + + storagepoolBulk.Add(new UpdateOneModel(storagePoolFilter, storagePoolUpdate) + { + IsUpsert = true + }); + } + + storagepoolBulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var storagePoolResult = await _database.HostStoragePool().BulkWriteAsync(storagepoolBulk, cancellationToken: cancellationToken); + } + + // physicaldisks + + if (storagePools is not null && storagePools.Any()) + { + var physicalDiskBulk = new List>(); + + foreach (var storagePool in storagePools) + { + var storagePoolId = await _database.HostStoragePool() + .Find(p => p.Host == hostEntity.Id && p.UniqueId == storagePool.UniqueId) + .Project(p => p.Id) + .FirstOrDefaultAsync(); + + if (storagePool.PhysicalDisks is not null && storagePool.PhysicalDisks.Any()) + { + foreach (var physicalDisk in storagePool.PhysicalDisks) + { + var physicalDiskFilter = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.StoragePool, storagePoolId), + Builders.Filter.Eq(x => x.UniqueId, physicalDisk.UniqueId) + }); + + List? states = null; + + if (physicalDisk.States is not null) + { + states = new List(); + + foreach (var state in physicalDisk.States) + { + states.Add(state.ToString()); + } + } + + var physicalDiskUpdate = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.StoragePool, storagePoolId) + .SetOnInsert(p => p.UniqueId, physicalDisk.UniqueId) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + + .Set(p => p.DeviceId, physicalDisk.DeviceId) + .Set(p => p.Name, physicalDisk.FriendlyName) + .Set(p => p.Manufacturer, physicalDisk.Manufacturer) + .Set(p => p.Model, physicalDisk.Model) + .Set(p => p.Media, physicalDisk.MediaType.ToString()) + .Set(p => p.Bus, physicalDisk.BusType.ToString()) + .Set(p => p.Health, physicalDisk.Health.ToString()) + .Set(p => p.Usage, physicalDisk.Usage) + .Set(p => p.Location, physicalDisk.PhysicalLocation) + .Set(p => p.Serial, physicalDisk.SerialNumber) + .Set(p => p.Firmware, physicalDisk.FirmwareVersion) + .Set(p => p.Size, physicalDisk.Size) + .Set(p => p.AllocatedSize, physicalDisk.AllocatedSize) + .Set(p => p.Footprint, physicalDisk.VirtualDiskFootprint) + .Set(p => p.LogicalSectorSize, physicalDisk.LogicalSectorSize) + .Set(p => p.PhysicalSectorSize, physicalDisk.PhysicalSectorSize) + .Set(p => p.States, states); + + physicalDiskBulk.Add(new UpdateOneModel(physicalDiskFilter, physicalDiskUpdate) + { + IsUpsert = true + }); + } + } + } + + physicalDiskBulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var physicalDiskResult = await _database.HostStoragePoolPhysicalDisk().BulkWriteAsync(physicalDiskBulk, cancellationToken: cancellationToken); + } + + // virtual disks + + if (storagePools is not null && storagePools.Any()) + { + var virtualDiskBulk = new List>(); + + foreach (var storagePool in storagePools) + { + if (storagePool.VirtualDisks is not null && storagePool.VirtualDisks.Any()) + { + foreach (var virtualDisk in storagePool.VirtualDisks) + { + var storagePoolId = await _database.HostStoragePool() + .Find(p => p.Host == hostEntity.Id && p.UniqueId == storagePool.UniqueId) + .Project(p => p.Id) + .FirstOrDefaultAsync(); + + var virtualDiskFilter = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.StoragePool, storagePoolId), + Builders.Filter.Eq(x => x.UniqueId, virtualDisk.UniqueId) + }); + + List? states = null; + + if (virtualDisk.States is not null) + { + states = new List(); + + foreach (var state in virtualDisk.States) + { + states.Add(state.ToString()); + } + } + + var virtualDiskUpdate = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.StoragePool, storagePoolId) + .SetOnInsert(p => p.UniqueId, virtualDisk.UniqueId) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + + .Set(p => p.Name, virtualDisk.FriendlyName) + .Set(p => p.Health, virtualDisk.Health.ToString()) + .Set(p => p.Access, virtualDisk.AccessType.ToString()) + .Set(p => p.Provisioning, virtualDisk.ProvisioningType.ToString()) + .Set(p => p.PhysicalRedundancy, virtualDisk.PhysicalDiskRedundancy) + .Set(p => p.Resiliency, virtualDisk.ResiliencySettingName) + .Set(p => p.Deduplication, virtualDisk.Deduplication) + .Set(p => p.Snapshot, virtualDisk.IsSnapshot) + .Set(p => p.Size, virtualDisk.Size) + .Set(p => p.AllocatedSize, virtualDisk.AllocatedSize) + .Set(p => p.Footprint, virtualDisk.FootprintOnPool) + .Set(p => p.ReadCacheSize, virtualDisk.ReadCacheSize) + .Set(p => p.WriteCacheSize, virtualDisk.WriteCacheSize) + .Set(p => p.States, states); + + virtualDiskBulk.Add(new UpdateOneModel(virtualDiskFilter, virtualDiskUpdate) + { + IsUpsert = true + }); + } + } + } + + virtualDiskBulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var virtualDiskResult = await _database.HostStoragePoolVirtualDisk().BulkWriteAsync(virtualDiskBulk, cancellationToken: cancellationToken); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/SystemInfoHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/SystemInfoHandler.cs new file mode 100644 index 0000000..7abab05 --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/SystemInfoHandler.cs @@ -0,0 +1,49 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class SystemInfoHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public SystemInfoHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is SystemInfo systemInfo) + { + await OnSystemInfoAsync(sender, systemInfo, cancellationToken); + } + } + + private async ValueTask OnSystemInfoAsync(AgentSession session, SystemInfo? system, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var date = DateTime.Now; + + await _database.HostSystem().UpdateOneAsync(p => p.Host == hostEntity.Id, Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .Set(p => p.Update, date) + .Set(p => p.BootUpTime, system.LastBootUpTime) + .Set(p => p.LocalTime, system.LocalDateTime) + .Set(p => p.Processes, system.Processes) + .Set(p => p.License, system.License), new UpdateOptions + { + IsUpsert = true + }, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/TrapHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/TrapHandler.cs new file mode 100644 index 0000000..9d8b817 --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/TrapHandler.cs @@ -0,0 +1,289 @@ +using Insight.Agent.Enums; +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using Insight.Server.Models; +using MongoDB.Driver; +using System.Text; +using System.Text.RegularExpressions; +using static Insight.Server.Models.MonitorMessage; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class TrapHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public TrapHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is Trap trap) + { + await OnTrapAsync(sender, trap, cancellationToken); + } + } + + private async ValueTask OnTrapAsync(AgentSession session, Trap? trap, CancellationToken cancellationToken) + { + if (trap is null) return; + + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + if (TryParse(trap, out var monitoring) is false) + { + //_logger.LogWarning($"Failed to parse"); + } + + // insert monitoring log + var monitoringLog = new HostLogMonitoringEntity + { + Host = hostEntity.Id, + Insert = DateTime.Now, + Timestamp = monitoring?.Timestamp, + Category = monitoring?.Category?.ToString(), + Status = monitoring?.Status?.ToString(), + Hostname = monitoring?.Hostname, + Task = monitoring?.Subject, + Message = monitoring?.Message, + Dispatch = DispatchEnum.Pending.ToString() + }; + + await _database.HostLogMonitoring() + .InsertOneAsync(monitoringLog, cancellationToken: cancellationToken); + + // insert host log + var log = new HostLogEntity + { + Insert = monitoringLog.Insert, + Host = monitoringLog.Host, + Category = monitoringLog.Category, + Status = monitoringLog.Status, + Source = "Trap", + Message = monitoringLog?.Task, + Timestamp = monitoringLog?.Insert + }; + + if (monitoringLog?.Message is not null) log.Message += $"\n{monitoringLog.Message}"; + + await _database.HostLog() + .InsertOneAsync(log, cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + private static bool TryParse(Trap packet, out MonitorMessage? monitoring) + { + monitoring = null; + + if (packet is null || packet.Data is null || packet.Data.Any() is false) return false; + + monitoring = new MonitorMessage + { + Community = packet.Community, + Category = CategoryEnum.Monitoring, + Endpoint = packet.Endpoint, + Timestamp = packet.Timestamp + }; + + if (Enum.TryParse(packet.Community, true, out var application)) + { + monitoring.Application = application; + } + + StatusEnum? status; + string? task; + string? message; + + switch (application) + { + case ApplicationEnum.Acronis: + monitoring.Application = ApplicationEnum.Acronis; + if (ParseAcronis(packet.Data, out status, out task, out message) is false) return false; + break; + case ApplicationEnum.Veeam: + monitoring.Application = ApplicationEnum.Veeam; + if (ParseVeeam(packet.Data, out status, out task, out message) is false) return false; + break; + case ApplicationEnum.QNAP: + monitoring.Application = ApplicationEnum.QNAP; + monitoring.Category = CategoryEnum.System; + monitoring.Hostname = packet.Hostname; + if (ParseQnap(packet.Data, out status, out task, out message) is false) return false; + break; + default: + return false; + } + + monitoring.Status = status; + monitoring.Subject = task; + monitoring.Message = message; + + return true; + } + + private static bool ParseAcronis(List> data, out StatusEnum? status, out string? task, out string? message) + { + status = data[0].Value.ToLower() switch + { + "erfolgreich" => StatusEnum.Information, + "success" => StatusEnum.Information, + "information" => StatusEnum.Information, + "warnung" => StatusEnum.Warning, + "warning" => StatusEnum.Warning, + "fehler" => StatusEnum.Error, + "failed" => StatusEnum.Error, + "error" => StatusEnum.Error, + _ => null, + }; + + task = null; + message = null; + + var parsed = false; + + try + { + var trim = data[1].Value.Split(new string[] { ":" }, StringSplitOptions.None); + task = trim[1].Split(new string[] { "'" }, StringSplitOptions.None)[1].Split("'")[0].Trim(); + message = trim[1].Split(new string[] { "' " }, StringSplitOptions.None)[1].Trim(); + + parsed = true; + } + catch (Exception) + { + // skipped for base64 parse + } + + if (parsed) return true; + + try + { + var content = Regex.Replace(data[1].Value, @"\s+", ""); + var bytes = Enumerable.Range(0, content.Length) + .Where(x => x % 2 == 0) + .Select(x => Convert.ToByte(content.Substring(x, 2), 16)) + .ToArray(); + + content = Encoding.UTF8.GetString(bytes); + + var trim = content.Split(new string[] { ":" }, StringSplitOptions.None); + task = trim[1].Split(new string[] { "'" }, StringSplitOptions.None)[1].Split("'")[0].Trim(); + message = trim[1].Split(new string[] { "' " }, StringSplitOptions.None)[1].Trim(); + + parsed = true; + } + catch (Exception ex) + { + //_logger.LogError("{ex}", ex); + } + + if (parsed) return true; + return false; + } + + private static bool ParseVeeam(List> data, out StatusEnum? status, out string? task, out string? message) + { + status = null; + task = null; + message = null; + + var parsed = false; + + try + { + var summary = false; + + if (Guid.TryParse(data[0].Value, out _)) + summary = true; + + if (data[1].Value.ToLower() == "backup configuration job") + return false; + + status = (summary ? data[2].Value.ToLower() : data[3].Value.ToLower()) switch + { + "success" => StatusEnum.Information, + "warning" => StatusEnum.Warning, + "failed" => StatusEnum.Error, + "error" => StatusEnum.Error, + _ => null, + }; + + task = data[1].Value; + parsed = true; + } + catch (Exception) + { + + } + + if (parsed) return true; + return false; + } + + private static bool ParseQnap(List> data, out StatusEnum? status, out string? task, out string? message) + { + status = StatusEnum.Information; + task = null; + message = string.Empty; + + var parsed = false; + + try + { + var keywords = new Dictionary + { + { "power", StatusEnum.Information }, + { "rights", StatusEnum.Information }, + { "added", StatusEnum.Information }, + { "changed", StatusEnum.Information }, + { "password", StatusEnum.Information }, + { "firmware", StatusEnum.Information }, + { "restarting", StatusEnum.Information }, + { "detected", StatusEnum.Warning }, + { "external", StatusEnum.Warning }, + { "threshold", StatusEnum.Warning }, + { "file system", StatusEnum.Warning }, + { "raid", StatusEnum.Warning }, + { "full", StatusEnum.Error }, + { "failure", StatusEnum.Error }, + { "failed", StatusEnum.Error }, + { "resyncing", StatusEnum.Error }, + { "degraded", StatusEnum.Error }, + { "error", StatusEnum.Error }, + { "without error", StatusEnum.Information }, + { "ncsi", StatusEnum.Information } + }; + + foreach (var key in keywords) + { + if (Regex.IsMatch(string.Concat(data).ToLowerInvariant(), $@"\b{key.Key}\b")) + { + status = key.Value; + } + } + + foreach (var kv in data) + { + message += kv.Value; + } + + parsed = true; + } + catch (Exception ex) + { + //_logger.LogError("{ex}", ex); + } + + if (parsed) return true; + return false; + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/UpdateHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/UpdateHandler.cs new file mode 100644 index 0000000..f1fe72d --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/UpdateHandler.cs @@ -0,0 +1,121 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class UpdateHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public UpdateHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is UpdateList updates) + { + await OnUpdatesAsync(sender, updates, cancellationToken); + } + } + + private async ValueTask OnUpdatesAsync(AgentSession session, UpdateList updates, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var batch = ObjectId.GenerateNewId().ToString(); + var date = DateTime.Now; + + var bulk = new List>(); + + if (updates is not null) + { + if (updates.Installed is not null && updates.Installed.Any()) + { + foreach (var update in updates.Installed) + { + var filterDefinition = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Serial, update.Id) + }); + + var updateDefinition = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Serial, update.Id) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + + .Set(p => p.Pending, false) + .Set(p => p.Name, update.Name) + .Set(p => p.Description, update.Description) + .Set(p => p.SupportUrl, update.SupportUrl) + .Set(p => p.Result, update.Result.ToString()) + .Set(p => p.Date, update.Date); + + bulk.Add(new UpdateOneModel(filterDefinition, updateDefinition) + { + IsUpsert = true + }); + } + } + + if (updates.Pending is not null && updates.Pending.Any()) + { + foreach (var update in updates.Pending) + { + var filterDefinition = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Serial, update.Id) + }); + + var updateDefinition = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Serial, update.Id) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + + .Set(p => p.Pending, true) + .Set(p => p.Name, update.Name) + .Set(p => p.Description, update.Description) + .Set(p => p.SupportUrl, update.SupportUrl) + .Set(p => p.Result, update.Result?.ToString()) + + .Set(p => p.Type, update.Type.ToString()) + .Set(p => p.Size, update.Size) + .Set(p => p.IsDownloaded, update.IsDownloaded) + .Set(p => p.CanRequestUserInput, update.CanRequestUserInput) + .Set(p => p.RebootBehavior, update.RebootBehavior?.ToString()) + + .Set(p => p.Date, update.Date); + + bulk.Add(new UpdateOneModel(filterDefinition, updateDefinition) + { + IsUpsert = true + }); + } + } + } + + bulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var result = await _database.HostUpdate().BulkWriteAsync(bulk, cancellationToken: cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/UserHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/UserHandler.cs new file mode 100644 index 0000000..5e09d66 --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/UserHandler.cs @@ -0,0 +1,188 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class UserHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public UserHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is UserList users) + { + await OnUsersAsync(sender, users, cancellationToken); + } + } + + private async ValueTask OnUsersAsync(AgentSession session, List? users, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var batch = ObjectId.GenerateNewId().ToString(); + var date = DateTime.Now; + + // users + + if (users is not null && users.Any()) + { + var userBulk = new List>(); + + foreach (var user in users) + { + var userFilter = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Domain, user?.Domain), + Builders.Filter.Eq(x => x.Name, user?.Name) + }); + + var userUpdate = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Domain, user?.Domain) + .SetOnInsert(p => p.Name, user?.Name) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + + .Set(p => p.Sid, user?.Sid) + .Set(p => p.FullName, user?.FullName) + .Set(p => p.Description, user?.Description) + .Set(p => p.Status, user?.Status) + .Set(p => p.LocalAccount, user?.LocalAccount) + .Set(p => p.Disabled, user?.Disabled) + .Set(p => p.Lockout, user?.Lockout) + .Set(p => p.PasswordChangeable, user?.PasswordChangeable) + .Set(p => p.PasswordExpires, user?.PasswordExpires) + .Set(p => p.PasswordRequired, user?.PasswordRequired); + + userBulk.Add(new UpdateOneModel(userFilter, userUpdate) + { + IsUpsert = true + }); + } + + userBulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var userResult = await _database.HostSystemUser().BulkWriteAsync(userBulk, cancellationToken: cancellationToken); + } + + // groups + + if (users is not null && users.Any()) + { + var groupBulk = new List>(); + + var distinctGroups = users.SelectMany(p => p.Groups) + .GroupBy(p => new { p?.Domain, p?.Name }) + .Select(p => p.First()); + + foreach (var group in distinctGroups) + { + var groupFilter = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Domain, group?.Domain), + Builders.Filter.Eq(x => x.Name, group?.Name) + }); + + var groupUpdate = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Domain, group?.Domain) + .SetOnInsert(p => p.Name, group?.Name) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + + .Set(p => p.Sid, group?.Sid) + .Set(p => p.Description, group?.Description) + .Set(p => p.LocalAccount, group?.LocalAccount); + + groupBulk.Add(new UpdateOneModel(groupFilter, groupUpdate) + { + IsUpsert = true + }); + } + + groupBulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var groupResult = await _database.HostSystemGroup().BulkWriteAsync(groupBulk, cancellationToken: cancellationToken); + } + + // relations + + if (users is not null && users.Any()) + { + var relationBulk = new List>(); + + foreach (var user in users) + { + var userId = await _database.HostSystemUser() + .Find(p => p.Host == hostEntity.Id && p.Domain == user.Domain && p.Name == user.Name) + .Project(p => p.Id) + .FirstOrDefaultAsync(); + + if (user.Groups is not null && user.Groups.Any()) + { + foreach (var group in user.Groups) + { + var groupId = await _database.HostSystemGroup() + .Find(p => p.Host == hostEntity.Id && p.Domain == group.Domain && p.Name == group.Name) + .Project(p => p.Id) + .FirstOrDefaultAsync(); + + var relationFilter = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.User, userId), + Builders.Filter.Eq(x => x.Group, groupId) + }); + + var relationUpdate = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.User, userId) + .SetOnInsert(p => p.Group, groupId) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch); + + relationBulk.Add(new UpdateOneModel(relationFilter, relationUpdate) + { + IsUpsert = true + }); + } + } + } + + relationBulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var relationResult = await _database.HostSystemUserSystemGroup().BulkWriteAsync(relationBulk, cancellationToken: cancellationToken); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/VideocardHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/VideocardHandler.cs new file mode 100644 index 0000000..bf8e648 --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/VideocardHandler.cs @@ -0,0 +1,77 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class VideocardHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public VideocardHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is VideocardList videocards) + { + await OnVideocardsAsync(sender, videocards, cancellationToken); + } + } + + private async ValueTask OnVideocardsAsync(AgentSession session, List? videocards, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var batch = ObjectId.GenerateNewId().ToString(); + var date = DateTime.Now; + + var bulk = new List>(); + + if (videocards is not null && videocards.Any()) + { + foreach (var videocard in videocards) + { + var filterDefinition = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.Name, videocard.Model) + }); + + var updateDefinition = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.Name, videocard.Model) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + .Set(p => p.Company, null) + .Set(p => p.Memory, videocard.Memory) + .Set(p => p.Driver, videocard.DriverVersion) + .Set(p => p.Date, videocard.DriverDate); + + bulk.Add(new UpdateOneModel(filterDefinition, updateDefinition) + { + IsUpsert = true + }); + } + + bulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var result = await _database.HostVideocard().BulkWriteAsync(bulk, cancellationToken: cancellationToken); + } + } + } +} diff --git a/src/Server/Insight.Server/Network/Handlers/Agent/VirtualMaschineHandler.cs b/src/Server/Insight.Server/Network/Handlers/Agent/VirtualMaschineHandler.cs new file mode 100644 index 0000000..a49e08d --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Agent/VirtualMaschineHandler.cs @@ -0,0 +1,173 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Insight.Server.Network.Handlers.Agent +{ + public class VirtualMaschineHandler : IAgentMessageHandler + { + private readonly IMongoDatabase _database; + + public VirtualMaschineHandler(IMongoDatabase database) + { + _database = database; + } + + public async ValueTask HandleAsync(AgentSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IAgentMessage + { + if (message is VirtualMaschineList virtualMaschines) + { + await OnVirtualMaschinesAsync(sender, virtualMaschines, cancellationToken); + } + } + + private async ValueTask OnVirtualMaschinesAsync(AgentSession session, List? virtualMaschines, CancellationToken cancellationToken) + { + var agentEntity = await _database.Agent().Find(Builders.Filter.Eq(p => p.Id, session.Id)).FirstOrDefaultAsync(cancellationToken); + if (agentEntity is null) return; + + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Agent, agentEntity.Id)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return; + + var batch = ObjectId.GenerateNewId().ToString(); + var date = DateTime.Now; + + // virtual maschines + if (virtualMaschines is not null && virtualMaschines.Any()) + { + var virtualMaschineBulk = new List>(); + + foreach (var virtualMaschine in virtualMaschines) + { + var virtualMaschineFilter = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.UniqueId, virtualMaschine.Id.ToString()) + }); + + var virtualMaschineUpdate = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.UniqueId, virtualMaschine.Id.ToString()) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + + .Set(p => p.Name, virtualMaschine?.Name) + .Set(p => p.Notes, virtualMaschine?.Notes) + .Set(p => p.Enabled, virtualMaschine?.Enabled?.ToString()) + .Set(p => p.EnabledDefault, virtualMaschine?.EnabledDefault?.ToString()) + .Set(p => p.Health, virtualMaschine?.HealthState?.ToString()) + .Set(p => p.Status, virtualMaschine?.Status) + .Set(p => p.OnTime, virtualMaschine?.OnTime) + .Set(p => p.ReplicationState, virtualMaschine?.ReplicationState?.ToString()) + .Set(p => p.ReplicationHealth, virtualMaschine?.ReplicationHealth?.ToString()) + .Set(p => p.ConfigurationVersion, virtualMaschine?.ConfigurationVersion) + .Set(p => p.IntegrationServicesVersionState, virtualMaschine?.IntegrationServicesVersionState?.ToString()) + .Set(p => p.ProcessId, virtualMaschine?.ProcessId) + .Set(p => p.NumberOfProcessors, virtualMaschine?.NumberOfProcessors) + .Set(p => p.ProcessorLoad, virtualMaschine?.ProcessorLoad) + .Set(p => p.MemoryAvailable, virtualMaschine?.MemoryAvailable) + .Set(p => p.MemoryUsage, virtualMaschine?.MemoryUsage) + .Set(p => p.InstallDate, virtualMaschine?.InstallDate) + .Set(p => p.ConfigurationVersion, virtualMaschine?.ConfigurationVersion) + .Set(p => p.TimeOfLastStateChange, virtualMaschine?.TimeOfLastStateChange) + .Set(p => p.LastReplicationTime, virtualMaschine?.LastReplicationTime) + .Set(p => p.Os, virtualMaschine?.GuestOperatingSystem); + + virtualMaschineBulk.Add(new UpdateOneModel(virtualMaschineFilter, virtualMaschineUpdate) + { + IsUpsert = true + }); + } + + virtualMaschineBulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var virtualMaschineResult = await _database.HostHypervisorVirtualMaschine().BulkWriteAsync(virtualMaschineBulk, cancellationToken: cancellationToken); + } + + // virtual maschine configurations + + if (virtualMaschines is not null && virtualMaschines.Any()) + { + var configurationBulk = new List>(); + + foreach (var virtualmaschine in virtualMaschines) + { + var virtualMaschineId = await _database.HostHypervisorVirtualMaschine() + .Find(p => p.Host == hostEntity.Id && p.UniqueId == virtualmaschine.Id.ToString()) + .Project(p => p.Id) + .FirstOrDefaultAsync(); + + if (virtualmaschine.Configurations is not null && virtualmaschine.Configurations.Any()) + { + foreach (var config in virtualmaschine.Configurations) + { + var configFilter = Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Eq(x => x.VirtualMaschine, virtualMaschineId), + Builders.Filter.Eq(x => x.UniqueId, config.Id) + }); + + // custom "notes" concat + string notes = string.Empty; + if (config?.Notes is not null) foreach (var n in config.Notes) notes += n; + + var configUpdate = Builders.Update + .SetOnInsert(p => p.Insert, date) + .SetOnInsert(p => p.Host, hostEntity.Id) + .SetOnInsert(p => p.VirtualMaschine, virtualMaschineId) + .SetOnInsert(p => p.UniqueId, config.Id) + .Set(p => p.Update, date) + .Set(p => p.Batch, batch) + + .Set(p => p.ParentId, config.ParentId) + .Set(p => p.Type, config.Type) + .Set(p => p.Name, config.Name) + .Set(p => p.Notes, notes) + .Set(p => p.CreationTime, config.CreationTime) + .Set(p => p.Generation, config.Generation) + .Set(p => p.Architecture, config.Architecture) + .Set(p => p.SecureBootEnabled, config.SecureBootEnabled) + .Set(p => p.IsAutomaticSnapshot, config.IsAutomaticSnapshot) + .Set(p => p.AutomaticStartupAction, config.AutomaticStartupAction?.ToString()) + .Set(p => p.AutomaticShutdownAction, config.AutomaticShutdownAction?.ToString()) + .Set(p => p.AutomaticRecoveryAction, config.AutomaticRecoveryAction?.ToString()) + .Set(p => p.AutomaticSnapshotsEnabled, config.AutomaticSnapshotsEnabled) + .Set(p => p.BaseBoardSerialNumber, config.BaseBoardSerialNumber) + .Set(p => p.BIOSSerialNumber, config.BIOSSerialNumber) + .Set(p => p.BIOSGUID, config.BIOSGUID) + .Set(p => p.ConfigurationDataRoot, config.ConfigurationDataRoot) + .Set(p => p.ConfigurationFile, config.ConfigurationFile) + .Set(p => p.GuestStateDataRoot, config.GuestStateDataRoot) + .Set(p => p.GuestStateFile, config.GuestStateFile) + .Set(p => p.SnapshotDataRoot, config.SnapshotDataRoot) + .Set(p => p.SuspendDataRoot, config.SuspendDataRoot) + .Set(p => p.SwapFileDataRoot, config.SwapFileDataRoot); + + configurationBulk.Add(new UpdateOneModel(configFilter, configUpdate) + { + IsUpsert = true + }); + } + } + } + + configurationBulk.Add(new DeleteManyModel(Builders.Filter.And(new List> + { + Builders.Filter.Eq(x => x.Host, hostEntity.Id), + Builders.Filter.Ne(x => x.Batch, batch) + }))); + + var configurationResult = await _database.HostVirtualMaschineConfig().BulkWriteAsync(configurationBulk, cancellationToken: cancellationToken); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/Handlers/Web/ConsoleProxyHandler.cs b/src/Server/Insight.Server/Network/Handlers/Web/ConsoleProxyHandler.cs new file mode 100644 index 0000000..416ee10 --- /dev/null +++ b/src/Server/Insight.Server/Network/Handlers/Web/ConsoleProxyHandler.cs @@ -0,0 +1,103 @@ +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using Insight.Web.Interfaces; +using Insight.Web.Messages; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; +using Vaitr.Bus; +using Vaitr.Network; + +namespace Insight.Server.Network.Handlers.Web +{ + public class ConsoleProxyHandler : IWebMessageHandler + { + private readonly List _subscriptions = new(); + + private readonly ISessionPool _agentPool; + private readonly ISessionPool _webPool; + private readonly IMongoDatabase _database; + private readonly Bus _bus; + private readonly ILogger _logger; + + public ConsoleProxyHandler( + ISessionPool agentPool, + ISessionPool webPool, + IMongoDatabase database, + Bus bus, + ILogger logger) + { + _agentPool = agentPool; + _webPool = webPool; + _database = database; + _bus = bus; + _logger = logger; + + _subscriptions.Add(_bus.SubscribeAsync(OnConsoleQueryAsync, null)); + } + + public async ValueTask HandleAsync(WebSession sender, TMessage message, CancellationToken cancellationToken) where TMessage : IWebMessage + { + if (message is ConsoleQueryProxyRequest consoleRequest) + { + await OnConsoleQueryRequestAsync(sender, consoleRequest, cancellationToken); + } + } + + private async ValueTask OnConsoleQueryRequestAsync(WebSession session, ConsoleQueryProxyRequest request, CancellationToken cancellationToken) + { + // get host + var hostEntity = await _database.Host() + .Find(Builders + .Filter + .Eq(p => p.Id, request.HostId)) + .FirstOrDefaultAsync(cancellationToken); + + if (hostEntity is null) + { + _logger.LogWarning("hostEntity is null"); + return; + } + + // get agent + var agentEntity = await _database.Agent() + .Find(Builders + .Filter + .Eq(p => p.Id, hostEntity.Agent)) + .FirstOrDefaultAsync(cancellationToken); + + if (agentEntity is null) + { + _logger.LogWarning("agentEntity is null"); + return; + } + + // check if agent online + if (_agentPool.FirstOrDefault(p => p.Value.Id == agentEntity.Id).Value is not AgentSession agent) return; + + // send "real" packet to agent + await agent.SendAsync(new ConsoleQueryRequest + { + Id = request.Id, + HostId = request.HostId, + Query = request.Query + }, cancellationToken); + } + + private async ValueTask OnConsoleQueryAsync(ConsoleQuery query, CancellationToken cancellationToken) + { + // check if web online + if (_webPool.FirstOrDefault().Value is not WebSession web) return; + + await web.SendAsync(new ConsoleQueryProxy + { + Id = query.Id, + HostId = query.HostId, + Query = query.Query, + Data = query.Data, + Errors = query.Errors, + HadErrors = query.HadErrors + }, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Network/WebSession.cs b/src/Server/Insight.Server/Network/WebSession.cs new file mode 100644 index 0000000..8c2e31c --- /dev/null +++ b/src/Server/Insight.Server/Network/WebSession.cs @@ -0,0 +1,55 @@ +using Insight.Web.Interfaces; +using Insight.Web.Messages; +using Microsoft.Extensions.Logging; +using Vaitr.Network; + +namespace Insight.Server.Network; + +public class WebSession : TcpSession +{ + public string? Id { get; set; } + + private readonly IEnumerable> _handlers; + + public WebSession(IEnumerable> handlers, ISerializer serializer, ILogger logger) : base(serializer, logger) + { + _handlers = handlers; + } + + protected override async ValueTask OnConnectedAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Web ({ep?}) connected", RemoteEndPoint); + } + + protected override async ValueTask OnDisconnectedAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Web ({ep?}) disconnected", RemoteEndPoint); + } + + protected override async ValueTask OnSentAsync(IPacketContext context, CancellationToken cancellationToken) + { + await base.OnSentAsync(context, cancellationToken); + } + + protected override async ValueTask OnReceivedAsync(IPacketContext context, CancellationToken cancellationToken) + { + await base.OnReceivedAsync(context, cancellationToken); + + foreach (var handler in _handlers) + { + try + { + await handler.HandleAsync(this, context.Packet, cancellationToken); + } + catch (Exception ex) + { + _logger.LogWarning("Web ({ep?}) {ex}", RemoteEndPoint, ex.ToString()); + } + } + } + + protected override async ValueTask OnHeartbeatAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Web ({ep?}) Heartbeat", RemoteEndPoint); + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Program.cs b/src/Server/Insight.Server/Program.cs new file mode 100644 index 0000000..6a0659c --- /dev/null +++ b/src/Server/Insight.Server/Program.cs @@ -0,0 +1,135 @@ +using Insight.Agent.Interfaces; +using Insight.Agent.Messages; +using Insight.Domain.Constants; +using Insight.Infrastructure; +using Insight.Server.Extensions; +using Insight.Server.Network; +using Insight.Server.Network.Handlers.Agent; +using Insight.Server.Network.Handlers.Web; +using Insight.Server.Services; +using Insight.Web.Interfaces; +using Insight.Web.Messages; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Net; +using Vaitr.Bus; +using Vaitr.Network; +using Vaitr.Network.Hosting; + +namespace Insight.Server +{ + internal class Program + { + public static async Task Main(string[] args) + { + var builder = Host.CreateDefaultBuilder(args); + builder.UseWindowsService(); + builder.UseSystemd(); + + builder.ConfigureAppConfiguration(options => + { + options.Defaults(); + }); + + builder.ConfigureLogging(options => + { + options.ClearProviders(); + options.SetMinimumLevel(LogLevel.Trace); + + options.AddSimpleConsole(options => + { + options.IncludeScopes = true; + options.SingleLine = true; + options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff "; + }); + + options.AddFile($"{Configuration.AppDirectory?.FullName}/" + "logs/server_{Date}.log", LogLevel.Trace, fileSizeLimitBytes: 104857600, retainedFileCountLimit: 10, outputTemplate: "{Timestamp:o} [{Level:u3}] {Message} {NewLine}{Exception}"); + }); + + builder.ConfigureServices((host, services) => + { + //var databaseLoggerFactory = LoggerFactory.Create(b => + //{ + // b.AddSimpleConsole(); + // b.SetMinimumLevel(LogLevel.Debug); + //}); + + // INFRASTRUCTURE + services.AddDatabase(host.Configuration); + + // AGENT SERVER + services.UseHostedServer(options => + { + options.Address = IPAddress.Any; + options.Port = host.Configuration.GetValue(Appsettings.AgentServerPort) ?? throw new Exception($"{Appsettings.AgentServerPort} value not set (appsettings)"); + options.Keepalive = 10000; + options.Timeout = 30000; + options.Backlog = 128; + + options.Encryption = Encryption.Tls12; + options.Certificate = host.Configuration.GetValue(Appsettings.AgentServerCertificate) ?? throw new Exception($"{Appsettings.AgentServerCertificate} value not set (appsettings)"); + options.CertificatePassword = host.Configuration.GetValue(Appsettings.AgentServerCertificatePassword) ?? throw new Exception($"{Appsettings.AgentServerCertificatePassword} value not set (appsettings)"); + + options.UseSerializer, IAgentMessage>(); + }); + + services.AddSingleton(); + services.AddSingleton, AgentHandler>(); + services.AddSingleton, DriveHandler>(); + services.AddSingleton, Network.Handlers.Agent.EventHandler>(); + services.AddSingleton, InterfaceHandler>(); + services.AddSingleton, MainboardHandler>(); + services.AddSingleton, MemoryHandler>(); + services.AddSingleton, OperationSystemHandler>(); + services.AddSingleton, PrinterHandler>(); + services.AddSingleton, ProcessorHandler>(); + services.AddSingleton, ServiceHandler>(); + services.AddSingleton, SessionHandler>(); + services.AddSingleton, SoftwareHandler>(); + services.AddSingleton, StoragePoolHandler>(); + services.AddSingleton, SystemInfoHandler>(); + services.AddSingleton, TrapHandler>(); + services.AddSingleton, UpdateHandler>(); + services.AddSingleton, UserHandler>(); + services.AddSingleton, VideocardHandler>(); + services.AddSingleton, VirtualMaschineHandler>(); + services.AddSingleton, ConsoleHandler>(); + + // WEB (FRONTEND-PROXY) SERVER + services.UseHostedServer(options => + { + options.Address = IPAddress.Any; + options.Port = host.Configuration.GetValue(Appsettings.WebServerPort) ?? throw new Exception($"{Appsettings.WebServerPort} value not set (appsettings)"); + options.Keepalive = 10000; + options.Timeout = 30000; + options.Backlog = 128; + + options.Encryption = Encryption.Tls12; + options.Certificate = host.Configuration.GetValue(Appsettings.WebServerCertificate) ?? throw new Exception($"{Appsettings.WebServerCertificate} value not set (appsettings)"); + options.CertificatePassword = host.Configuration.GetValue(Appsettings.WebServerCertificatePassword) ?? throw new Exception($"{Appsettings.WebServerCertificatePassword} value not set (appsettings)"); + + options.UseSerializer, IWebMessage>(); + }); + + services.AddSingleton, ConsoleProxyHandler>(); + + // DISPATCH + services.AddHostedService(); + services.AddHostedService(); + + // GLOBAL DEPENDENCIES + services.AddSingleton(); + services.AddTransient(provider => new HttpClient(new HttpClientHandler + { + ClientCertificateOptions = ClientCertificateOption.Manual, + ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true + })); + }); + + var host = builder.Build(); + await host.RunAsync().ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Properties/launchSettings.json b/src/Server/Insight.Server/Properties/launchSettings.json new file mode 100644 index 0000000..c95fc24 --- /dev/null +++ b/src/Server/Insight.Server/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "Development": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Production": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Services/DispatchService.cs b/src/Server/Insight.Server/Services/DispatchService.cs new file mode 100644 index 0000000..7d3b6c4 --- /dev/null +++ b/src/Server/Insight.Server/Services/DispatchService.cs @@ -0,0 +1,158 @@ +using Insight.Agent.Enums; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; + +namespace Insight.Server.Services +{ + internal class DispatchService : BackgroundService + { + private readonly HttpClient _httpClient; + private readonly IMongoDatabase _database; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public DispatchService(HttpClient httpClient, IMongoDatabase database, IConfiguration configuration, ILogger logger) + { + _httpClient = httpClient; + _database = database; + _configuration = configuration; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + _logger.LogTrace("ExecuteAsync"); + + var enabled = _configuration.GetValue(Appsettings.DispatchWebmatic) ?? throw new Exception($"{Appsettings.DispatchWebmatic} value not set (appsettings)"); + if (enabled is false) return; + + try + { + while (cancellationToken.IsCancellationRequested is false) + { + await DispatchAsync(cancellationToken); + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + } + } + catch (OperationCanceledException) { } + catch (Exception) { } + } + + private async ValueTask DispatchAsync(CancellationToken cancellationToken) + { + _logger.LogTrace($"DispatchAsync"); + + var pendings = await _database.HostLogMonitoring() + .Find(Builders + .Filter.Eq(p => p.Dispatch, DispatchEnum.Pending.ToString())) + .Limit(10) + .ToListAsync(cancellationToken); + + if (pendings is null || pendings.Any() is false) return; + + foreach (var entity in pendings) + { + try + { + var result = await SendAsync(entity, default); + + await _database.HostLogMonitoring() + .UpdateOneAsync(Builders.Filter + .Eq(p => p.Id, entity.Id), Builders + .Update + .Set(p => p.Dispatch, result.ToString()), cancellationToken: default); + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + _logger.LogTrace(ex.StackTrace); + } + finally + { + // webmatic safety offset + await Task.Delay(TimeSpan.FromSeconds(1), default); + } + } + } + + private async ValueTask SendAsync(HostLogMonitoringEntity monitoring, CancellationToken cancellationToken) + { + _logger.LogTrace($"SendAsync ({monitoring})"); + + var monitoringApi = Monitoring.LogUri; + var monitoringContent = new List>(); + var monitoringResult = new List>(); + + // adjust by category + if (Enum.TryParse(monitoring.Category, true, out var monitoringCategory) is false) return DispatchEnum.Failure; + + if (monitoringCategory == CategoryEnum.Monitoring) monitoringApi = Monitoring.StatusUri; + + // set category (if log) + if (monitoringApi == Monitoring.LogUri) monitoringContent.Add(new KeyValuePair("category", monitoringCategory.ToString())); + + // host resolve + var hostEntity = await _database.Host().Find(Builders.Filter.Eq(p => p.Id, monitoring.Host?.ToString())).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return DispatchEnum.Failure; + + // customer resolve + var customerEntity = await _database.Customer().Find(Builders.Filter.Eq(p => p.Id, hostEntity.Customer)).FirstOrDefaultAsync(cancellationToken); + if (hostEntity is null) return DispatchEnum.Failure; + + // set host name if no remote host set + if (string.IsNullOrEmpty(monitoring.Hostname)) monitoring.Hostname = hostEntity.Name; + + // remove any domain from hostname + if (monitoring.Hostname is not null && monitoring.Hostname.Contains('.')) monitoring.Hostname = monitoring.Hostname.Split(".")[0]; + + // add customer tag to hostname + monitoring.Hostname += $".{customerEntity.Tag}"; + + // if task null, set hostname + if (string.IsNullOrEmpty(monitoring.Task)) monitoring.Task = monitoring.Hostname; + + // insert hostname as computer-name (lowercase) + monitoringContent.Add(new KeyValuePair("computer_name", monitoring.Hostname.ToLower())); + + // insert converted status (api styled) + if (Enum.TryParse(monitoring.Status, true, out var monitoringStatus) is false) return DispatchEnum.Failure; + + monitoringContent.Add(monitoringStatus switch + { + StatusEnum.Information => new KeyValuePair("status", monitoringApi == Monitoring.StatusUri ? "erfolgreich" : "info"), + StatusEnum.Warning => new KeyValuePair("status", monitoringApi == Monitoring.StatusUri ? "Interaktion" : "warning"), + StatusEnum.Error => new KeyValuePair("status", monitoringApi == Monitoring.StatusUri ? "fehlgeschlagen" : "error"), + _ => throw new NotImplementedException(nameof(monitoringStatus)) + }); + + // insert task, timestamp, message, + monitoringContent.Add(new KeyValuePair("task", monitoring.Task)); + + if (monitoring.Timestamp is not null) + { + monitoringContent.Add(new KeyValuePair("timestamp", monitoring.Timestamp.Value.ToLocalTime().ToString())); + } + + if (string.IsNullOrWhiteSpace(monitoring.Message) is false) + { + monitoringContent.Add(new KeyValuePair("message", monitoring.Message)); + } + + // send message + var result = await _httpClient.PostAsync(monitoringApi, new FormUrlEncodedContent(monitoringContent), default); + + monitoringResult.Add(new KeyValuePair("HttpStatusCode", result.StatusCode.ToString())); + monitoringResult.Add(new KeyValuePair("HttpResponseMessage", await result.Content.ReadAsStringAsync(default))); + + // if content != "OK" + if (result is null || result.IsSuccessStatusCode == false) return DispatchEnum.Failure; + + // success + return DispatchEnum.Success; + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/Services/JobService.cs b/src/Server/Insight.Server/Services/JobService.cs new file mode 100644 index 0000000..3e800a3 --- /dev/null +++ b/src/Server/Insight.Server/Services/JobService.cs @@ -0,0 +1,79 @@ +using Insight.Agent.Messages; +using Insight.Infrastructure; +using Insight.Infrastructure.Entities; +using Insight.Server.Extensions; +using Insight.Server.Network; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using MongoDB.Driver; +using Vaitr.Network; + +namespace Insight.Server.Services +{ + internal class JobService : BackgroundService + { + private readonly ISessionPool _agentPool; + private readonly IMongoDatabase _database; + private readonly ILogger _logger; + + public JobService(ISessionPool agentPool, IMongoDatabase database, ILogger logger) + { + _agentPool = agentPool; + _database = database; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + _logger.LogTrace("ExecuteAsync"); + + var jobs = new List + { + Task.Run(async () => + { + while (cancellationToken.IsCancellationRequested is false) + { + try + { + foreach (var agent in await GetAssignedAgentsAsync(cancellationToken)) + { + await agent.SendAsync(new GetInventory(), cancellationToken); + } + } + catch (OperationCanceledException) { } + catch (Exception) { } + finally + { + await Task.Delay(TimeSpan.FromHours(1), cancellationToken); + } + } + }, default) + }; + + try + { + await Task.WhenAll(jobs).ConfigureAwait(false); + } + catch (OperationCanceledException) { } + catch (Exception) { } + } + + private async ValueTask> GetAssignedAgentsAsync(CancellationToken cancellationToken) + { + var valid = new List(); + + await Async.ParallelForEach(_agentPool.Where(p => p.Value.Id is not null), async x => + { + var host = await _database.Host() + .Find(Builders.Filter.Eq(p => p.Agent, x.Value.Id)) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + + if (host is null) return; + valid.Add(x.Value); + }); + + return valid; + } + } +} \ No newline at end of file diff --git a/src/Server/Insight.Server/appsettings.Development.json b/src/Server/Insight.Server/appsettings.Development.json new file mode 100644 index 0000000..74223d4 --- /dev/null +++ b/src/Server/Insight.Server/appsettings.Development.json @@ -0,0 +1,12 @@ +{ + "database": "mongodb://db.insight.local:27017", + "agent.server.port": 3002, + "agent.server.certificate": "localhost.pfx", + "agent.server.certificate.password": "Webmatic12", + + "web.server.port": 3001, + "web.server.certificate": "localhost.pfx", + "web.server.certificate.password": "Webmatic12", + + "dispatch.webmatic": false +} \ No newline at end of file diff --git a/src/Server/Insight.Server/appsettings.json b/src/Server/Insight.Server/appsettings.json new file mode 100644 index 0000000..b0ee8b6 --- /dev/null +++ b/src/Server/Insight.Server/appsettings.json @@ -0,0 +1,12 @@ +{ + "database": "mongodb://127.0.0.1:27017", + "agent.server.port": 3002, + "agent.server.certificate": "localhost.pfx", + "agent.server.certificate.password": "Webmatic12", + + "web.server.port": 3001, + "web.server.certificate": "localhost.pfx", + "web.server.certificate.password": "Webmatic12", + + "dispatch.webmatic": false +} \ No newline at end of file diff --git a/src/Setup/Insight.Setup.Windows/Constants/Deploy.cs b/src/Setup/Insight.Setup.Windows/Constants/Deploy.cs new file mode 100644 index 0000000..724759b --- /dev/null +++ b/src/Setup/Insight.Setup.Windows/Constants/Deploy.cs @@ -0,0 +1,49 @@ +namespace Insight.Setup.Constants +{ + public static class Deploy + { + public static class Runtime + { + public static class Core + { + public const string Version = "7.0.2"; + public const string Download = "https://download.visualstudio.microsoft.com/download/pr/df7da01f-1f17-4728-92b7-778e9607da8f/7c18246830f8c78591f02f25aa368dcf/dotnet-runtime-7.0.2-win-x64.exe"; + + public static DirectoryInfo Directory => new DirectoryInfo($@"{Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)}/dotnet/shared/Microsoft.NETCore.App"); + } + + public static class Asp + { + public const string Version = "7.0.2"; + public const string Download = "https://download.visualstudio.microsoft.com/download/pr/3ecad4f7-1342-4688-ae4a-38908c61f4a2/391a9010acad2e312e3d1e766bedfac7/aspnetcore-runtime-7.0.2-win-x64.exe"; + + public static DirectoryInfo Directory => new DirectoryInfo($@"{Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)}/dotnet/shared/Microsoft.AspNetCore.App"); + } + } + + public static class Updater + { + public const string Name = "Updater"; + public const string ServiceName = "insight_updater"; + public const string Description = "Insight Updater"; + + public static Uri UpdateHref(Uri api) => new($"{api.AbsoluteUri}/{Name}/windows"); + } + + public static class Agent + { + public const string Name = "Agent"; + public const string ServiceName = "insight_agent"; + public const string Description = "Insight Agent"; + } + + public static DirectoryInfo GetAppDirectory(string appName) + => new($"{Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)}/Webmatic/Insight/{appName}"); + + public static FileInfo GetAppExecutable(string appName) + => new($"{GetAppDirectory(appName).FullName}/{appName.ToLower()}.exe"); + + public static Uri GetUpdateHref(Uri api, string appName) + => new($"{api.AbsoluteUri}/update/{appName.ToLower()}/windows"); + } +} \ No newline at end of file diff --git a/src/Setup/Insight.Setup.Windows/Insight.Setup.Windows.csproj b/src/Setup/Insight.Setup.Windows/Insight.Setup.Windows.csproj new file mode 100644 index 0000000..92fa03c --- /dev/null +++ b/src/Setup/Insight.Setup.Windows/Insight.Setup.Windows.csproj @@ -0,0 +1,44 @@ + + + + Exe + net7.0 + true + enable + setup + Insight.Setup + Insight + 2023.7.3.0 + + + + none + + + + none + + + + + PreserveNewest + true + PreserveNewest + + + PreserveNewest + true + PreserveNewest + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Setup/Insight.Setup.Windows/Program.cs b/src/Setup/Insight.Setup.Windows/Program.cs new file mode 100644 index 0000000..8c488d7 --- /dev/null +++ b/src/Setup/Insight.Setup.Windows/Program.cs @@ -0,0 +1,58 @@ +using Insight.Setup.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Runtime.Versioning; + +namespace Insight.Setup.Windows +{ + [SupportedOSPlatform("windows")] + public class Program + { + public static async Task Main(string[] args) + { + await Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration(options => + { + options.Sources.Clear(); + options.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); + options.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", optional: true, reloadOnChange: true); + + options.AddCommandLine(args, new Dictionary() + { + { "-deploy", "deploy" }, + { "--deploy", "deploy" } + }); + }) + .ConfigureLogging(options => + { + options.ClearProviders(); + options.SetMinimumLevel(LogLevel.Trace); + + options.AddSimpleConsole(options => + { + options.IncludeScopes = true; + options.SingleLine = true; + options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff "; + }); + + options.AddFilter("Microsoft", LogLevel.Warning); + }) + .ConfigureServices((host, services) => + { + // SERVICES + services.AddHostedService(); + + // GLOBALS + services.AddTransient(provider => new HttpClient(new HttpClientHandler + { + ClientCertificateOptions = ClientCertificateOption.Manual, + ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true + })); + }) + .Build() + .RunAsync(); + } + } +} \ No newline at end of file diff --git a/src/Setup/Insight.Setup.Windows/Properties/launchSettings.json b/src/Setup/Insight.Setup.Windows/Properties/launchSettings.json new file mode 100644 index 0000000..a655c14 --- /dev/null +++ b/src/Setup/Insight.Setup.Windows/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "profiles": { + "Development": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Production": { + "commandName": "Project" + }, + "Development Install": { + "commandName": "Project", + "commandLineArgs": "--deploy install", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Development Uninstall": { + "commandName": "Project", + "commandLineArgs": "--deploy uninstall", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Production Install": { + "commandName": "Project", + "commandLineArgs": "--deploy install" + } + } +} \ No newline at end of file diff --git a/src/Setup/Insight.Setup.Windows/Services/Deployment.cs b/src/Setup/Insight.Setup.Windows/Services/Deployment.cs new file mode 100644 index 0000000..57a8047 --- /dev/null +++ b/src/Setup/Insight.Setup.Windows/Services/Deployment.cs @@ -0,0 +1,477 @@ +using Insight.Domain.Models; +using System.Diagnostics; +using System.IO.Compression; +using System.Net.Http.Json; +using System.Runtime.Versioning; +using System.ServiceProcess; +using System.Text.Json; + +namespace Insight.Setup.Services +{ + public static class Deployment + { + [SupportedOSPlatform("windows")] + public static class Windows + { + public static class Service + { + private static bool ServiceExistence(string serviceName) + { + try + { + if (ServiceController.GetServices().Any(s => s.ServiceName.Equals(serviceName, StringComparison.InvariantCultureIgnoreCase))) return true; + return false; + } + catch (Exception) { } + + return false; + } + + private static bool SetServiceState(string app, ServiceControllerStatus status, TimeSpan timeout) + { + try + { + using var sc = ServiceController.GetServices().FirstOrDefault(s => s.ServiceName.Equals(app, StringComparison.InvariantCultureIgnoreCase)); + if (sc is null) return false; + + if (sc.Status != status) + { + switch (status) + { + case ServiceControllerStatus.Running: + sc.Start(); + break; + + case ServiceControllerStatus.Stopped: + sc.Stop(); + break; + } + + sc.WaitForStatus(status, timeout); + } + + return true; + } + catch (Exception) { } + + return false; + } + + private static async ValueTask InstallAsync(FileInfo bin, string serviceName, string displayName, string description, bool autorun) + { + if (bin.Exists is false) return false; + if (ServiceExistence(serviceName)) return false; + + var args = @$"/C sc create {serviceName} binPath= ""{bin.FullName}"" DisplayName= ""{displayName}"""; + args += $@" && sc description {serviceName} ""{description}"""; + args += $@" && sc config {serviceName} start= {(autorun ? "auto" : "demand")}"; + args += $@" && sc start {serviceName}"; + + try + { + using var process = new System.Diagnostics.Process() + { + StartInfo = new ProcessStartInfo + { + WindowStyle = ProcessWindowStyle.Normal, + FileName = "cmd.exe", + Arguments = args, + Verb = "runas", + RedirectStandardOutput = true, + UseShellExecute = false + } + }; + + process.Start(); + + var output = await process.StandardOutput.ReadToEndAsync(); + + if (Directory.GetParent(bin.FullName) is not DirectoryInfo serviceDir) return false; + + return ServiceExistence(serviceName); // may return output as return model if existence false + } + catch (Exception) + { + return false; + } + } + + public static async ValueTask InstallAsync(HttpClient httpClient, Uri api, FileInfo bin, string serviceName, string serviceDisplayName, string serviceDescription, bool autorun, CancellationToken cancellationToken) + { + var result = new InstallResult + { + Api = api?.ToString(), + SourceDirectory = bin.Directory?.FullName, + App = bin.Name, + ServiceName = serviceName, + Autorun = autorun + }; + + try + { + if (ServiceExistence(serviceName)) + { + result.Errors.Add("Service already installed"); + return result; + } + + var response = await httpClient.GetFromJsonAsync(api, cancellationToken); + if (response is null) + { + result.ApiErrors.Add("not available / response null"); + return result; + } + + // get update file (bytes) to memory + using var update = await httpClient.GetAsync(response.Uri, cancellationToken); + if (update is null) + { + result.ApiErrors.Add("update source not available"); + return result; + } + + result.ApiAvailable = true; + + // read update archive to temp (overwrite) + var temp = Directory.CreateTempSubdirectory(); + var updateFile = new FileInfo($@"{temp.FullName}/{bin.Name}.zip"); + + await File.WriteAllBytesAsync(updateFile.FullName, await update.Content.ReadAsByteArrayAsync(cancellationToken), cancellationToken); + + // extract update archive from temp to app dir (overwrite) + ZipFile.ExtractToDirectory(updateFile.FullName, bin.Directory?.FullName, true); + + // delete temp folder + if (temp.Exists) temp.Delete(true); + + // install service with windows api + if (await InstallAsync(bin, serviceName, serviceDisplayName, serviceDescription, autorun) is false) + { + result.Errors.Add("installation failed"); + return result; + } + + result.Success = true; + } + catch (Exception ex) + { + result.Errors.Add(ex.Message); + } + + return result; + } + + public static async ValueTask UpdateAsync(HttpClient httpClient, Uri api, FileInfo bin, string serviceName, CancellationToken cancellationToken) + { + var result = new UpdateResult + { + Api = api?.ToString(), + SourceDirectory = bin.Directory?.FullName, + App = bin.Name, + ServiceName = serviceName + }; + + try + { + // check if service exists + if (ServiceExistence(serviceName) is false) + { + result.UpdateErrors.Add("service not found"); + return result; + } + + // get service update details + var response = await httpClient.GetFromJsonAsync(api, cancellationToken); + if (response is null) + { + result.ApiErrors.Add("not available / response null"); + return result; + } + + result.ApiAvailable = true; + + // check if local binary exists + if (bin is null) + { + result.UpdateErrors.Add("source binary not found"); + return result; + } + + // get local file binary version + if (FileVersionInfo.GetVersionInfo(bin.FullName).FileVersion is not string binVersionString) + { + result.UpdateErrors.Add("source binary fileversion not valid"); + return result; + } + + // compare local against update version, skip lower or equal update version + var actualVersion = Version.Parse(binVersionString); + if (actualVersion >= response.Version) + { + result.Success = true; + return result; + } + else + { + result.UpdateAvailable = true; + } + + // get update file (bytes) to memory + using var update = await httpClient.GetAsync(response.Uri, cancellationToken); + if (update is null) + { + result.ApiErrors.Add("update source not available"); + return result; + } + + // stop service + if (SetServiceState(serviceName, ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(10)) is false) + { + result.UpdateErrors.Add("service control failed / failed to stop service"); + return result; + } + + // read update archive to temp (overwrite) + var temp = Directory.CreateTempSubdirectory(); + var updateFile = new FileInfo($@"{temp.FullName}/{bin.Name}.zip"); + + await File.WriteAllBytesAsync(updateFile.FullName, await update.Content.ReadAsByteArrayAsync(cancellationToken), cancellationToken); + + // extract update archive from temp to app dir (overwrite) + ZipFile.ExtractToDirectory(updateFile.FullName, bin.Directory?.FullName, true); + + // delete temp folder + if (temp.Exists) temp.Delete(true); + + // start updateds service + if (SetServiceState(serviceName, ServiceControllerStatus.Running, TimeSpan.FromSeconds(10)) is false) + { + result.UpdateErrors.Add("service control failed / failed to start service"); + return result; + } + + result.Success = true; + } + catch (Exception ex) + { + result.UpdateErrors.Add(ex.Message); + } + + return result; + } + + public static async ValueTask UninstallAsync(string serviceName) + { + var result = new UninstallResult + { + ServiceName = serviceName + }; + + try + { + if (ServiceExistence(serviceName) is false) + { + result.Errors.Add("service not found"); + return result; + } + + if (SetServiceState(serviceName, ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(60)) is false) + { + result.Errors.Add("service control failed / failed to stop service"); + return result; + } + + using var process = new System.Diagnostics.Process() + { + StartInfo = new ProcessStartInfo + { + WindowStyle = ProcessWindowStyle.Normal, + FileName = "cmd.exe", + Arguments = $"/C sc delete {serviceName}", + Verb = "runas", + RedirectStandardOutput = true, + UseShellExecute = false + } + }; + + process.Start(); + + var output = await process.StandardOutput.ReadToEndAsync(); + + // may return output as return model if existence true + + if (ServiceExistence(serviceName)) + { + result.Errors.Add("service still existing"); + } + + result.Success = true; + } + catch (Exception ex) + { + result.Errors.Add(ex.Message); + } + + return result; + } + } + + public static class Process + { + public static bool IsRunning(FileInfo bin) + { + if (bin.Exists is false) return false; + + var matched = System.Diagnostics.Process.GetProcessesByName(bin.FullName); + + if (matched is null || matched.Any() is false) return false; + + if (matched.Any(p => + p.MainModule is not null && + p.MainModule.FileName is not null && + p.MainModule.FileName.Equals(bin.FullName, + StringComparison.InvariantCultureIgnoreCase))) return true; + + return false; + } + + public static bool Start(FileInfo binary) + { + try + { + if (IsRunning(binary) is false) return false; + + using var process = System.Diagnostics.Process.Start(binary.FullName); + return true; + } + catch (Exception) { } + + return false; + } + + public static bool Stop(FileInfo bin, TimeSpan timeout) + { + try + { + if (IsRunning(bin) is false) return false; + + var matched = System.Diagnostics.Process.GetProcessesByName(bin.FullName); + + if (matched is null || matched.Any() is false) return true; + + foreach (var procsInfo in matched.Where(p => + p.MainModule is not null && + p.MainModule.FileName is not null && + p.MainModule.FileName.Equals(bin.FullName, StringComparison.InvariantCultureIgnoreCase))) + { + if (procsInfo.CloseMainWindow()) procsInfo.WaitForExit((int)timeout.TotalMilliseconds); + if (procsInfo.HasExited is false) procsInfo.Kill(true); + } + + return true; + } + catch (Exception) { } + + return false; + } + + public static async ValueTask UpdateAsync(HttpClient httpClient, Uri api, FileInfo bin, CancellationToken cancellationToken) + { + if (IsRunning(bin) is false) return false; + + var response = await httpClient.GetFromJsonAsync(api.AbsoluteUri, new JsonSerializerOptions + { + IncludeFields = true + }, cancellationToken); + + if (response is null) return false; + + Version actualVersion; + + if (FileVersionInfo.GetVersionInfo(bin.FullName).FileVersion is not string binVersionString) return false; + + try + { + actualVersion = Version.Parse(binVersionString); + if (actualVersion >= response.Version) return false; + + using var update = await httpClient.GetAsync(response.Uri, cancellationToken); + if (update is null) return false; + + Stop(bin, TimeSpan.FromSeconds(60)); + + // read update archive to temp (overwrite) + var temp = Directory.CreateTempSubdirectory(); + var updateFile = new FileInfo($@"{temp.FullName}/{bin.Name}.zip"); + + await File.WriteAllBytesAsync(updateFile.FullName, await update.Content.ReadAsByteArrayAsync(cancellationToken), cancellationToken); + + // extract update archive from temp to app dir (overwrite) + ZipFile.ExtractToDirectory(updateFile.FullName, bin.Directory?.FullName, true); + + // delete temp folder + if (temp.Exists) temp.Delete(true); + + // rewrite with options to start user session process + //Start(app, directory, TimeSpan.FromSeconds(60)); + + return true; + } + catch (Exception) { } + + return false; + } + + public static bool Delete(FileInfo bin, TimeSpan timeout) + { + try + { + Stop(bin, timeout); + bin.Delete(); + + return true; + } + catch (Exception) { } + + return false; + } + } + } + + public class InstallResult + { + public string? Api { get; set; } + public string? SourceDirectory { get; set; } + public string? App { get; set; } + public string? ServiceName { get; set; } + public bool Autorun { get; set; } + + public bool ApiAvailable { get; set; } = false; + public bool Success { get; set; } = false; + public List ApiErrors { get; } = new(); + public List Errors { get; } = new(); + } + + public class UpdateResult + { + public string? Api { get; set; } + public string? SourceDirectory { get; set; } + public string? App { get; set; } + public string? ServiceName { get; set; } + + public bool ApiAvailable { get; set; } = false; + public bool UpdateAvailable { get; set; } = false; + public bool Success { get; set; } = false; + public List ApiErrors { get; } = new(); + public List UpdateErrors { get; } = new(); + } + + public class UninstallResult + { + public string? ServiceName { get; set; } + + public bool Success { get; set; } = false; + public List Errors { get; } = new(); + } + } +} \ No newline at end of file diff --git a/src/Setup/Insight.Setup.Windows/Services/SetupService.cs b/src/Setup/Insight.Setup.Windows/Services/SetupService.cs new file mode 100644 index 0000000..54df73b --- /dev/null +++ b/src/Setup/Insight.Setup.Windows/Services/SetupService.cs @@ -0,0 +1,247 @@ +using Insight.Setup.Constants; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Diagnostics; +using System.Runtime.Versioning; + +namespace Insight.Setup.Services +{ + [SupportedOSPlatform("windows")] + internal class SetupService : BackgroundService + { + private readonly Uri _uri; + + private readonly HttpClient _httpClient; + private readonly IHostApplicationLifetime _lifetime; + private readonly ILogger _logger; + + public SetupService(HttpClient httpClient, IHostApplicationLifetime lifetime, IConfiguration configuration, ILogger logger) + { + _httpClient = httpClient; + _lifetime = lifetime; + _uri = configuration.GetValue("api") ?? new Uri("https://insight.webmatic.de/api"); //throw new Exception($"api value not set (appsettings)"); + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + _logger.LogTrace("ExecuteAsync"); + + Console.WriteLine("1: Install"); + Console.WriteLine("2: Update"); + Console.WriteLine("3: Uninstall"); + + Console.WriteLine("\n"); + + var key = Console.ReadKey(); + + Console.Clear(); + + try + { + switch (key.Key) + { + case ConsoleKey.NumPad1: + case ConsoleKey.D1: + { + await InstallAsync(cancellationToken); + break; + } + case ConsoleKey.NumPad2: + case ConsoleKey.D2: + { + await UpdateAsync(cancellationToken); + break; + } + case ConsoleKey.NumPad3: + case ConsoleKey.D3: + { + await UninstallAsync(); + break; + } + default: + { + Console.WriteLine("invalid selection"); + break; + } + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + finally + { + _lifetime.StopApplication(); + } + } + + private async ValueTask InstallAsync(CancellationToken cancellationToken) + { + Console.WriteLine("Install Runtime"); + await InstallRuntimeAsync(cancellationToken); + + Console.WriteLine("Install ASP Runtime"); + await InstallAspRuntimeAsync(cancellationToken); + + // UPDATER + Console.WriteLine("Install Updater"); + + var updaterResult = await Deployment.Windows.Service.InstallAsync( + _httpClient, + Deploy.GetUpdateHref(_uri, Deploy.Updater.Name), + Deploy.GetAppExecutable(Deploy.Updater.Name), + Deploy.Updater.ServiceName, + Deploy.Updater.Description, + Deploy.Updater.Description, + true, + cancellationToken); + + Console.WriteLine($"Updater: {updaterResult.Success}"); + Console.WriteLine($"Updater: {string.Concat(updaterResult.Errors)}"); + + // AGENT + Console.WriteLine("Install Agent"); + + var agentResult = await Deployment.Windows.Service.InstallAsync( + _httpClient, + Deploy.GetUpdateHref(_uri, Deploy.Agent.Name), + Deploy.GetAppExecutable(Deploy.Agent.Name), + Deploy.Agent.ServiceName, + Deploy.Agent.Description, + Deploy.Agent.Description, + true, + cancellationToken); + + Console.WriteLine($"Agent: {agentResult}"); + Console.WriteLine($"Agent: {string.Concat(agentResult.Errors)}"); + } + + private async ValueTask UpdateAsync(CancellationToken cancellationToken) + { + // UPDATER + Console.WriteLine("Update Updater"); + + var updateResult = await Deployment.Windows.Service.UpdateAsync( + _httpClient, + Deploy.GetUpdateHref(_uri, Deploy.Updater.Name), + Deploy.GetAppExecutable(Deploy.Updater.Name), + Deploy.Updater.ServiceName, + cancellationToken); + + Console.WriteLine($"Result: {updateResult}"); + + // AGENT + Console.WriteLine("Update Agent"); + + var agentResult = await Deployment.Windows.Service.UpdateAsync( + _httpClient, + Deploy.GetUpdateHref(_uri, Deploy.Agent.Name), + Deploy.GetAppExecutable(Deploy.Agent.Name), + Deploy.Agent.ServiceName, + cancellationToken); + + Console.WriteLine($"Result: {agentResult}"); + } + + private static async ValueTask UninstallAsync() + { + // UPDATER + Console.WriteLine("Uninstall Updater"); + + var updaterResult = await Deployment.Windows.Service.UninstallAsync(Deploy.Updater.ServiceName); + Console.WriteLine($"Result: {updaterResult}"); + + // AGENT + Console.WriteLine("Uninstall Agent"); + + var agentResult = await Deployment.Windows.Service.UninstallAsync(Deploy.Agent.ServiceName); + Console.WriteLine($"Result: {agentResult}"); + } + + private async ValueTask InstallRuntimeAsync(CancellationToken cancellationToken) + { + // try get dotnet folders + var coreDirectory = Deploy.Runtime.Core.Directory; + + // Runtime Installation Check... + if (coreDirectory.Exists && coreDirectory.EnumerateDirectories().Any(x => x.Name == Deploy.Runtime.Core.Version)) return; + + // Downloading Runtime... + var queryResult = await _httpClient.GetAsync(Deploy.Runtime.Core.Download, cancellationToken); + + var tempDir = Directory.CreateTempSubdirectory(); + + await File.WriteAllBytesAsync( + $@"{tempDir.FullName}/runtime.exe", + await queryResult.Content.ReadAsByteArrayAsync(cancellationToken), cancellationToken); + + // Installing Runtime... + var runtimeFile = new FileInfo($@"{tempDir.FullName}/runtime.exe"); + + using var process = new Process() + { + StartInfo = new ProcessStartInfo + { + FileName = runtimeFile.FullName, + ArgumentList = + { + @"/install", + @"/quiet", + @"/norestart" + }, + UseShellExecute = false, + RedirectStandardOutput = true + } + }; + + process.Start(); + await process.WaitForExitAsync(cancellationToken); + + if (runtimeFile.Exists) runtimeFile.Delete(); + } + + private async ValueTask InstallAspRuntimeAsync(CancellationToken cancellationToken) + { + // try get dotnet folders + var aspDirectory = Deploy.Runtime.Asp.Directory; + + // Runtime Installation Check... + if (aspDirectory.Exists && aspDirectory.EnumerateDirectories().Any(x => x.Name == Deploy.Runtime.Asp.Version)) return; + + // Downloading Runtime... + var queryResult = await _httpClient.GetAsync(Deploy.Runtime.Asp.Download, cancellationToken); + + var tempDir = Directory.CreateTempSubdirectory(); + + await File.WriteAllBytesAsync( + $@"{tempDir.FullName}/runtime.exe", + await queryResult.Content.ReadAsByteArrayAsync(cancellationToken), cancellationToken); + + // Installing Runtime... + var runtimeFile = new FileInfo($@"{tempDir.FullName}/runtime.exe"); + + using var process = new Process() + { + StartInfo = new ProcessStartInfo + { + FileName = runtimeFile.FullName, + ArgumentList = + { + @"/install", + @"/quiet", + @"/norestart" + }, + UseShellExecute = false, + RedirectStandardOutput = true + } + }; + + process.Start(); + await process.WaitForExitAsync(cancellationToken); + + if (runtimeFile.Exists) runtimeFile.Delete(); + } + } +} \ No newline at end of file diff --git a/src/Setup/Insight.Setup.Windows/appsettings.Development.json b/src/Setup/Insight.Setup.Windows/appsettings.Development.json new file mode 100644 index 0000000..8e8bc46 --- /dev/null +++ b/src/Setup/Insight.Setup.Windows/appsettings.Development.json @@ -0,0 +1,3 @@ +{ + "api": "http://127.0.0.1:5001/api" +} \ No newline at end of file diff --git a/src/Setup/Insight.Setup.Windows/appsettings.json b/src/Setup/Insight.Setup.Windows/appsettings.json new file mode 100644 index 0000000..a124a4b --- /dev/null +++ b/src/Setup/Insight.Setup.Windows/appsettings.json @@ -0,0 +1,3 @@ +{ + "api": "https://insight.webmatic.de/api" +} \ No newline at end of file diff --git a/src/Updater/Insight.Updater/Constants/Deploy.cs b/src/Updater/Insight.Updater/Constants/Deploy.cs new file mode 100644 index 0000000..b285b98 --- /dev/null +++ b/src/Updater/Insight.Updater/Constants/Deploy.cs @@ -0,0 +1,21 @@ +namespace Insight.Updater.Constants +{ + public static class Deploy + { + public static class Agent + { + public const string Name = "Agent"; + public const string ServiceName = "insight_agent"; + public const string Description = "Insight Agent"; + } + + public static DirectoryInfo GetAppDirectory(string appName) + => new($"{Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)}/Webmatic/Insight/{appName}"); + + public static FileInfo GetAppExecutable(string appName) + => new($"{GetAppDirectory(appName).FullName}/{appName.ToLower()}.exe"); + + public static Uri GetUpdateHref(Uri api, string appName) + => new($"{api.AbsoluteUri}/update/{appName.ToLower()}/windows"); + } +} \ No newline at end of file diff --git a/src/Updater/Insight.Updater/Insight.Updater.csproj b/src/Updater/Insight.Updater/Insight.Updater.csproj new file mode 100644 index 0000000..3e39aee --- /dev/null +++ b/src/Updater/Insight.Updater/Insight.Updater.csproj @@ -0,0 +1,52 @@ + + + + Exe + net7.0 + Insight + Insight.Updater + updater + 2023.7.3.0 + enable + enable + + + + none + + + + none + + + + + + + + + + Always + true + PreserveNewest + + + Always + true + PreserveNewest + + + + + + + + + + + + + + + + diff --git a/src/Updater/Insight.Updater/Program.cs b/src/Updater/Insight.Updater/Program.cs new file mode 100644 index 0000000..892f807 --- /dev/null +++ b/src/Updater/Insight.Updater/Program.cs @@ -0,0 +1,57 @@ +using Insight.Domain.Constants; +using Insight.Updater.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Insight.Updater.Windows +{ + internal class Program + { + public static async Task Main(string[] args) + { + var builder = Host.CreateDefaultBuilder(args); + builder.UseWindowsService(); + builder.UseSystemd(); + + builder.ConfigureAppConfiguration(options => + { + options.Sources.Clear(); + options.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); + options.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", optional: true, reloadOnChange: true); + }); + + builder.ConfigureLogging(options => + { + options.ClearProviders(); + options.SetMinimumLevel(LogLevel.Trace); + + options.AddSimpleConsole(options => + { + options.IncludeScopes = true; + options.SingleLine = true; + options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff "; + }); + + options.AddFile($"{Configuration.AppDirectory?.FullName}/" + "logs/updater_{Date}.log", LogLevel.Trace, fileSizeLimitBytes: 104857600, retainedFileCountLimit: 10, outputTemplate: "{Timestamp:o} [{Level:u3}] {Message} {NewLine}{Exception}"); + }); + + builder.ConfigureServices((host, services) => + { + // SERVICES + services.AddHostedService(); + + // GLOBALS + services.AddTransient(provider => new HttpClient(new HttpClientHandler + { + ClientCertificateOptions = ClientCertificateOption.Manual, + ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true + })); + }); + + var host = builder.Build(); + await host.RunAsync().ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/Updater/Insight.Updater/Properties/launchSettings.json b/src/Updater/Insight.Updater/Properties/launchSettings.json new file mode 100644 index 0000000..12b98ee --- /dev/null +++ b/src/Updater/Insight.Updater/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "Development": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Production": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/src/Updater/Insight.Updater/Services/UpdateService.cs b/src/Updater/Insight.Updater/Services/UpdateService.cs new file mode 100644 index 0000000..e88c598 --- /dev/null +++ b/src/Updater/Insight.Updater/Services/UpdateService.cs @@ -0,0 +1,360 @@ +using Insight.Domain.Models; +using Insight.Updater.Constants; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Diagnostics; +using System.IO.Compression; +using System.Net.Http.Json; +using System.Runtime.Versioning; +using System.ServiceProcess; +using System.Text.Json; + +namespace Insight.Updater.Services +{ + public class UpdateService : BackgroundService + { + private readonly Uri _uri; + + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public UpdateService(HttpClient httpClient, IConfiguration configuration, ILogger logger) + { + _httpClient = httpClient; + _uri = configuration.GetValue("api") ?? throw new Exception($"api value not set (appsettings)"); + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + _logger.LogTrace("ExecuteAsync"); + + while (cancellationToken.IsCancellationRequested is false) + { + try + { + UpdateResult? result = null; + + if (OperatingSystem.IsWindows()) result = await WindowsUpdateAsync(cancellationToken); + if (OperatingSystem.IsLinux()) result = await LinuxUpdateAsync(cancellationToken); + + _logger.LogInformation("Update Result: {result}", result?.Success); + if (result?.UpdateErrors is not null) + { + _logger.LogError("Update Errors: {errors}", string.Concat(result?.UpdateErrors)); + } + } + catch (OperationCanceledException) { } + catch (Exception ex) // may inform via client / api about errors + { + _logger.LogError("{ex}", ex.Message); + } + finally + { + await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken); + } + } + } + + [SupportedOSPlatform("windows")] + private async ValueTask WindowsUpdateAsync(CancellationToken cancellationToken) + { + return await Windows.Service.UpdateAsync( + _httpClient, + Deploy.GetUpdateHref(_uri, Deploy.Agent.Name), + Deploy.GetAppExecutable(Deploy.Agent.Name), + Deploy.Agent.ServiceName, + cancellationToken); + } + + [SupportedOSPlatform("linux")] + private ValueTask LinuxUpdateAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + [SupportedOSPlatform("windows")] + private static class Windows + { + public static class Service + { + private static bool ServiceExistence(string serviceName) + { + try + { + if (ServiceController.GetServices().Any(s => s.ServiceName.Equals(serviceName, StringComparison.InvariantCultureIgnoreCase))) return true; + return false; + } + catch (Exception) { } + + return false; + } + + private static bool SetServiceState(string app, ServiceControllerStatus status, TimeSpan timeout) + { + try + { + using var sc = ServiceController.GetServices().FirstOrDefault(s => s.ServiceName.Equals(app, StringComparison.InvariantCultureIgnoreCase)); + if (sc is null) return false; + + if (sc.Status != status) + { + switch (status) + { + case ServiceControllerStatus.Running: + sc.Start(); + break; + + case ServiceControllerStatus.Stopped: + sc.Stop(); + break; + } + + sc.WaitForStatus(status, timeout); + } + + return true; + } + catch (Exception) { } + + return false; + } + + public static async ValueTask UpdateAsync(HttpClient httpClient, Uri api, FileInfo bin, string serviceName, CancellationToken cancellationToken) + { + var result = new UpdateResult + { + Api = api?.ToString(), + SourceDirectory = bin.Directory?.FullName, + App = bin.Name, + ServiceName = serviceName + }; + + try + { + // check if service exists + if (ServiceExistence(serviceName) is false) + { + result.UpdateErrors.Add("service not found"); + return result; + } + + // get service update details + var response = await httpClient.GetFromJsonAsync(api, cancellationToken); + if (response is null) + { + result.ApiErrors.Add("not available / response null"); + return result; + } + + result.ApiAvailable = true; + + // check if local binary exists + if (bin is null) + { + result.UpdateErrors.Add("source binary not found"); + return result; + } + + // get local file binary version + if (FileVersionInfo.GetVersionInfo(bin.FullName).FileVersion is not string binVersionString) + { + result.UpdateErrors.Add("source binary fileversion not valid"); + return result; + } + + // compare local against update version, skip lower or equal update version + var actualVersion = Version.Parse(binVersionString); + if (actualVersion >= response.Version) + { + result.Success = true; + return result; + } + else + { + result.UpdateAvailable = true; + } + + // get update file (bytes) to memory + using var update = await httpClient.GetAsync(response.Uri, cancellationToken); + if (update is null) + { + result.ApiErrors.Add("update source not available"); + return result; + } + + // stop service + if (SetServiceState(serviceName, ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(10)) is false) + { + result.UpdateErrors.Add("service control failed / failed to stop service"); + return result; + } + + // read update archive to temp (overwrite) + var temp = Directory.CreateTempSubdirectory(); + var updateFile = new FileInfo($@"{temp.FullName}/{bin.Name}.zip"); + + await File.WriteAllBytesAsync(updateFile.FullName, await update.Content.ReadAsByteArrayAsync(cancellationToken), cancellationToken); + + // extract update archive from temp to app dir (overwrite) + ZipFile.ExtractToDirectory(updateFile.FullName, bin.Directory?.FullName, true); + + // delete temp folder + if (temp.Exists) temp.Delete(true); + + // start updateds service + if (SetServiceState(serviceName, ServiceControllerStatus.Running, TimeSpan.FromSeconds(10)) is false) + { + result.UpdateErrors.Add("service control failed / failed to start service"); + return result; + } + + result.Success = true; + } + catch (Exception ex) + { + result.UpdateErrors.Add(ex.Message); + } + + return result; + } + } + + public static class Process + { + public static bool IsRunning(FileInfo bin) + { + if (bin.Exists is false) return false; + + var matched = System.Diagnostics.Process.GetProcessesByName(bin.FullName); + + if (matched is null || matched.Any() is false) return false; + + if (matched.Any(p => + p.MainModule is not null && + p.MainModule.FileName is not null && + p.MainModule.FileName.Equals(bin.FullName, + StringComparison.InvariantCultureIgnoreCase))) return true; + + return false; + } + + public static bool Start(FileInfo binary) + { + try + { + if (IsRunning(binary) is false) return false; + + using var process = System.Diagnostics.Process.Start(binary.FullName); + return true; + } + catch (Exception) { } + + return false; + } + + public static bool Stop(FileInfo bin, TimeSpan timeout) + { + try + { + if (IsRunning(bin) is false) return false; + + var matched = System.Diagnostics.Process.GetProcessesByName(bin.FullName); + + if (matched is null || matched.Any() is false) return true; + + foreach (var procsInfo in matched.Where(p => + p.MainModule is not null && + p.MainModule.FileName is not null && + p.MainModule.FileName.Equals(bin.FullName, StringComparison.InvariantCultureIgnoreCase))) + { + if (procsInfo.CloseMainWindow()) procsInfo.WaitForExit((int)timeout.TotalMilliseconds); + if (procsInfo.HasExited is false) procsInfo.Kill(true); + } + + return true; + } + catch (Exception) { } + + return false; + } + + public static async ValueTask UpdateAsync(HttpClient httpClient, Uri api, FileInfo bin, CancellationToken cancellationToken) + { + if (IsRunning(bin) is false) return false; + + var response = await httpClient.GetFromJsonAsync(api.AbsoluteUri, new JsonSerializerOptions + { + IncludeFields = true + }, cancellationToken); + + if (response is null) return false; + + Version actualVersion; + + if (FileVersionInfo.GetVersionInfo(bin.FullName).FileVersion is not string binVersionString) return false; + + try + { + actualVersion = Version.Parse(binVersionString); + if (actualVersion >= response.Version) return false; + + using var update = await httpClient.GetAsync(response.Uri, cancellationToken); + if (update is null) return false; + + Stop(bin, TimeSpan.FromSeconds(60)); + + // read update archive to temp (overwrite) + var temp = Directory.CreateTempSubdirectory(); + var updateFile = new FileInfo($@"{temp.FullName}/{bin.Name}.zip"); + + await File.WriteAllBytesAsync(updateFile.FullName, await update.Content.ReadAsByteArrayAsync(cancellationToken), cancellationToken); + + // extract update archive from temp to app dir (overwrite) + ZipFile.ExtractToDirectory(updateFile.FullName, bin.Directory?.FullName, true); + + // delete temp folder + if (temp.Exists) temp.Delete(true); + + // rewrite with options to start user session process + //Start(app, directory, TimeSpan.FromSeconds(60)); + + return true; + } + catch (Exception) { } + + return false; + } + + public static bool Delete(FileInfo bin, TimeSpan timeout) + { + try + { + Stop(bin, timeout); + bin.Delete(); + + return true; + } + catch (Exception) { } + + return false; + } + } + } + + public class UpdateResult + { + public string? Api { get; set; } + public string? SourceDirectory { get; set; } + public string? App { get; set; } + public string? ServiceName { get; set; } + + public bool ApiAvailable { get; set; } = false; + public bool UpdateAvailable { get; set; } = false; + public bool Success { get; set; } = false; + public List ApiErrors { get; } = new(); + public List UpdateErrors { get; } = new(); + } + } +} \ No newline at end of file diff --git a/src/Updater/Insight.Updater/appsettings.Development.json b/src/Updater/Insight.Updater/appsettings.Development.json new file mode 100644 index 0000000..7f055cd --- /dev/null +++ b/src/Updater/Insight.Updater/appsettings.Development.json @@ -0,0 +1,3 @@ +{ + "api": "http://insight.local/api" +} \ No newline at end of file diff --git a/src/Updater/Insight.Updater/appsettings.json b/src/Updater/Insight.Updater/appsettings.json new file mode 100644 index 0000000..a124a4b --- /dev/null +++ b/src/Updater/Insight.Updater/appsettings.json @@ -0,0 +1,3 @@ +{ + "api": "https://insight.webmatic.de/api" +} \ No newline at end of file diff --git a/src/Web/Insight.Web.Assets/Insight.Web.Assets.csproj b/src/Web/Insight.Web.Assets/Insight.Web.Assets.csproj new file mode 100644 index 0000000..64f79ee --- /dev/null +++ b/src/Web/Insight.Web.Assets/Insight.Web.Assets.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + true + enable + Insight.Web.Assets + Insight.Web + Insight + 2023.9.14.0 + + + + + + + + diff --git a/src/Web/Insight.Web.Assets/Interfaces/IWebMessageHandler.cs b/src/Web/Insight.Web.Assets/Interfaces/IWebMessageHandler.cs new file mode 100644 index 0000000..2609347 --- /dev/null +++ b/src/Web/Insight.Web.Assets/Interfaces/IWebMessageHandler.cs @@ -0,0 +1,9 @@ +using Insight.Web.Messages; + +namespace Insight.Web.Interfaces +{ + public partial interface IWebMessageHandler + { + ValueTask HandleAsync(TSender sender, TMessage message, CancellationToken cancellationToken) where TMessage : IWebMessage; + } +} \ No newline at end of file diff --git a/src/Web/Insight.Web.Assets/Messages/Host/ConsoleProxy.cs b/src/Web/Insight.Web.Assets/Messages/Host/ConsoleProxy.cs new file mode 100644 index 0000000..0a5783f --- /dev/null +++ b/src/Web/Insight.Web.Assets/Messages/Host/ConsoleProxy.cs @@ -0,0 +1,43 @@ +using MemoryPack; + +namespace Insight.Web.Messages +{ + [MemoryPackUnion(1, typeof(ConsoleQueryProxy))] + [MemoryPackUnion(2, typeof(ConsoleQueryProxyRequest))] + public partial interface IWebMessage { } + + [MemoryPackable] + public partial class ConsoleQueryProxy : IWebMessage + { + [MemoryPackOrder(0)] + public string? Id { get; set; } + + [MemoryPackOrder(1)] + public string? HostId { get; set; } + + [MemoryPackOrder(2)] + public string? Query { get; set; } + + [MemoryPackOrder(3)] + public string? Data { get; set; } + + [MemoryPackOrder(4)] + public string? Errors { get; set; } + + [MemoryPackOrder(7)] + public bool HadErrors { get; set; } + } + + [MemoryPackable] + public partial class ConsoleQueryProxyRequest : IWebMessage + { + [MemoryPackOrder(0)] + public string? Id { get; set; } + + [MemoryPackOrder(1)] + public string? HostId { get; set; } + + [MemoryPackOrder(2)] + public string? Query { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/Insight.Web.Assets/Messages/IWebMessage.cs b/src/Web/Insight.Web.Assets/Messages/IWebMessage.cs new file mode 100644 index 0000000..6ac0a66 --- /dev/null +++ b/src/Web/Insight.Web.Assets/Messages/IWebMessage.cs @@ -0,0 +1,7 @@ +using MemoryPack; + +namespace Insight.Web.Messages +{ + [MemoryPackable] + public partial interface IWebMessage { } +} \ No newline at end of file diff --git a/src/Web/Insight.Web/.config/dotnet-tools.json b/src/Web/Insight.Web/.config/dotnet-tools.json new file mode 100644 index 0000000..34c3e19 --- /dev/null +++ b/src/Web/Insight.Web/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "7.0.5", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/src/Web/Insight.Web/App.razor b/src/Web/Insight.Web/App.razor new file mode 100644 index 0000000..ef3df96 --- /dev/null +++ b/src/Web/Insight.Web/App.razor @@ -0,0 +1,14 @@ + + + + + + + + Not found + +

404

+
+
+
+
diff --git a/src/Web/Insight.Web/Components/Cards/InfoCard.razor b/src/Web/Insight.Web/Components/Cards/InfoCard.razor new file mode 100644 index 0000000..207c6c9 --- /dev/null +++ b/src/Web/Insight.Web/Components/Cards/InfoCard.razor @@ -0,0 +1,47 @@ + + + + + + @if (Icon is not null) + { + + } + + + + @Key + + + + + + + + +@code { + [Parameter] + public string? Href { get; set; } + + [Parameter] + public EventCallback OnClick { get; set; } + + [Parameter] + public string? Icon { get; set; } + + [Parameter] + public Color? IconColor { get; set; } + + [Parameter] + public string? Key { get; set; } + + [Parameter] + public Color? KeyColor { get; set; } + + [Parameter] + public string? Text { get; set; } + + [Parameter] + public int Lines { get; set; } = 1; +} \ No newline at end of file diff --git a/src/Web/Insight.Web/Components/Cards/KeyValueCard.razor b/src/Web/Insight.Web/Components/Cards/KeyValueCard.razor new file mode 100644 index 0000000..01a4527 --- /dev/null +++ b/src/Web/Insight.Web/Components/Cards/KeyValueCard.razor @@ -0,0 +1,57 @@ +@typeparam T + + + + + + + @if (Icon is not null) + { + + } + + + + @Key + + + @if (Value is null) + { + @("-") + } + else + { + @Value + } + + + + + + + +@code{ + [Parameter] + public string? Href { get; set; } + + [Parameter] + public EventCallback OnClick { get; set; } + + [Parameter] + public string? Icon { get; set; } + + [Parameter] + public Color? IconColor { get; set; } + + [Parameter] + public string? Key { get; set; } + + [Parameter] + public Color? KeyColor { get; set; } + + [Parameter] + public T? Value { get; set; } + + [Parameter] + public Color? ValueColor { get; set; } +} \ No newline at end of file diff --git a/src/Web/Insight.Web/Components/Containers/BaseContainer.razor b/src/Web/Insight.Web/Components/Containers/BaseContainer.razor new file mode 100644 index 0000000..5d504ad --- /dev/null +++ b/src/Web/Insight.Web/Components/Containers/BaseContainer.razor @@ -0,0 +1,51 @@ +@Title + +@*@if (Loading) +{ + + +
+ +
+ + return; +}*@ + +
+
+
+@* + + + + @item.Text + + + + *@ + + + +
+ @if (LoadData is not null && DisableReload is false) + { +
+ @if (Loading) + { + + } + else + { + + } +
+ } +
+
+ +
+ @if (Content is not null) + { + @Content + } +
\ No newline at end of file diff --git a/src/Web/Insight.Web/Components/Containers/BaseContainer.razor.cs b/src/Web/Insight.Web/Components/Containers/BaseContainer.razor.cs new file mode 100644 index 0000000..8f5ab44 --- /dev/null +++ b/src/Web/Insight.Web/Components/Containers/BaseContainer.razor.cs @@ -0,0 +1,74 @@ +using Insight.Web.Constants; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Insight.Web.Components.Containers; + +public partial class BaseContainer +{ + [Parameter] + public string Title { get; set; } = Global.Name; + + [Parameter] + public List? Breadcrumbs { get; set; } + + [Parameter] + public RenderFragment? BreadcrumbAction { get; set; } + + [Parameter] + public RenderFragment? Content { get; set; } + + [Parameter] + public Func? LoadData { get; set; } + + [Parameter] + public bool DisableReload { get; set; } + + + private bool _loading; + private bool Loading + { + get + { + return _loading; + } + set + { + if (value != _loading) + { + _loading = value; + StateHasChanged(); + } + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await OnRefreshAsync(); + } + } + + private async Task OnRefreshAsync() + { + if (LoadData is null || Loading) return; + + Loading = true; + StateHasChanged(); + + //var start = Stopwatch.GetTimestamp(); + + await LoadData(); + + //var time = Stopwatch.GetElapsedTime(start); + + //if (time <= TimeSpan.FromSeconds(1)) + //{ + // await Task.Delay(TimeSpan.FromSeconds(1).Subtract(time)); + //} + + Loading = false; + StateHasChanged(); + } +} \ No newline at end of file diff --git a/src/Web/Insight.Web/Components/Containers/TableContainer.razor b/src/Web/Insight.Web/Components/Containers/TableContainer.razor new file mode 100644 index 0000000..a7d06ff --- /dev/null +++ b/src/Web/Insight.Web/Components/Containers/TableContainer.razor @@ -0,0 +1,66 @@ +@typeparam T + +@Title + +
+
+
+ + + @if (OnAdd.HasDelegate) + { + @if (AddBreadcrumbs is not null) + { + + } + + + } + +
+
+ +
+
+
+ + + + @if (Header is not null) + { + @Header + } + + + + Filter + + + Refresh + + + + + + @if (RowTemplate is not null) + { + @RowTemplate(context) + } + + @if (ActionTemplate is not null) + { + + @if (ActionTemplate is not null) + { + @ActionTemplate(context) + } + + } + + + + + + \ No newline at end of file diff --git a/src/Web/Insight.Web/Components/Containers/TableContainer.razor.cs b/src/Web/Insight.Web/Components/Containers/TableContainer.razor.cs new file mode 100644 index 0000000..bf75e8f --- /dev/null +++ b/src/Web/Insight.Web/Components/Containers/TableContainer.razor.cs @@ -0,0 +1,194 @@ +using Insight.Web.Constants; +using Insight.Web.Extensions; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Insight.Web.Components.Containers; + +public partial class TableContainer +{ + [Inject] private NavigationManager NavigationManager { get; init; } = default!; + + + [Parameter] + public string Title { get; set; } = Global.Name; + + [Parameter] + public List? Breadcrumbs { get; set; } + + [Parameter] + public Func>>? Data { get; set; } + + [Parameter] + public RenderFragment? Header { get; set; } + + [Parameter] + public RenderFragment? RowTemplate { get; set; } + + [Parameter] + public RenderFragment? ActionTemplate { get; set; } + + [Parameter] + public bool DisableAction { get; set; } + + + private List? AddBreadcrumbs { get; set; } + private MudTable? Table { get; set; } + private bool Loading { get; set; } + + private int CurrentPage { get; set; } + private int CurrentSize { get; set; } + + protected override void OnInitialized() + { + if (OnAdd.HasDelegate) + { + AddBreadcrumbs = new List() + { + new BreadcrumbItem("", "#", true), + new BreadcrumbItem("", "#", true) + }; + } + + if (NavigationManager.GetQueryString().TryGetValue("search", out var search)) + { + Search = search.ToString(); + } + + //if (NavigationManager.GetQueryString().TryGetValue("page", out var pageRaw)) + //{ + // if (int.TryParse(pageRaw, out var page)) + // { + // CurrentPage = page; + // } + //} + + //if (NavigationManager.GetQueryString().TryGetValue("size", out var sizeRaw)) + //{ + // if (int.TryParse(sizeRaw, out var size)) + // { + // CurrentSize = size == 0 ? 100 : size; + // } + //} + } + + private async Task> OnLoadDataAsync(TableState state) + { + if (Data is null) + { + throw new MissingMethodException(nameof(Data)); + } + + try + { + Loading = true; + + //state.Page = CurrentPage; + //Table.SetRowsPerPage(CurrentSize); + + var data = await Data(state); + + //CurrentPage = state.Page; + //CurrentSize = state.PageSize; + + //NavigationManager.ChangeQueryStringValue("page", state.Page.ToString()); + //NavigationManager.ChangeQueryStringValue("size", state.PageSize.ToString()); + + //if (state.SortLabel is not null) NavigationManager.ChangeQueryStringValue("sort", state.SortLabel.ToString()); + //if (state.SortDirection) NavigationManager.ChangeQueryStringValue("direction", state.SortDirection.ToString()); + + return data; + } + finally + { + Loading = false; + } + } + + // + + [Parameter] + public bool DisableRefresh { get; set; } + + private string? _search; + + [Parameter] + public string? Search + { + get => _search; + set + { + if (_search == value) return; + _search = value; + + SearchChanged.InvokeAsync(value); + } + } + + [Parameter] + public EventCallback SearchChanged { get; set; } + + private async Task SearchAsync(string? text) + { + Search = text; + + if (Loading) return; + await RefreshAsync(); + } + + public async Task RefreshAsync() + { + if (Loading || Table is null) return; + + Loading = true; + StateHasChanged(); + + //var start = Stopwatch.GetTimestamp(); + + await Table.ReloadServerData(); + + //var time = Stopwatch.GetElapsedTime(start); + + //if (time <= TimeSpan.FromSeconds(1)) + //{ + // await Task.Delay(TimeSpan.FromSeconds(1).Subtract(time)); + //} + + Loading = false; + StateHasChanged(); + } + + private void OnSearchReleased() + { + NavigationManager.ChangeQueryStringValue("search", Search); + } + + // + + [Parameter] public EventCallback OnFilter { get; set; } + + + [Parameter] + public bool Filtered { get; set; } + + private async Task OnFilterClick() + { + if (OnFilter.HasDelegate) + { + await OnFilter.InvokeAsync(this); + } + } + + // + + [Parameter] + public EventCallback OnAdd { get; set; } + + private async Task OnAddClick() + { + if (OnAdd.HasDelegate) + { + await OnAdd.InvokeAsync(this); + } + } +} \ No newline at end of file diff --git a/src/Web/Insight.Web/Components/Dialogs/ActionDialog.razor b/src/Web/Insight.Web/Components/Dialogs/ActionDialog.razor new file mode 100644 index 0000000..660fea5 --- /dev/null +++ b/src/Web/Insight.Web/Components/Dialogs/ActionDialog.razor @@ -0,0 +1,25 @@ +@if (@Actions is not null) +{ + + + + @Content + + + + + @Actions + + + +} +else +{ + + + + @Content + + + +} \ No newline at end of file diff --git a/src/Web/Insight.Web/Components/Dialogs/ActionDialog.razor.cs b/src/Web/Insight.Web/Components/Dialogs/ActionDialog.razor.cs new file mode 100644 index 0000000..f4734ab --- /dev/null +++ b/src/Web/Insight.Web/Components/Dialogs/ActionDialog.razor.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Components; + +namespace Insight.Web.Components.Dialogs; + +public partial class ActionDialog +{ + private bool _visible; + + [Parameter] + public bool Visible + { + get => _visible; + set + { + if (_visible == value) return; + _visible = value; + + VisibleChanged.InvokeAsync(value); + } + } + + [Parameter] + public EventCallback VisibleChanged { get; set; } + + [Parameter] + public RenderFragment? Content { get; set; } + + [Parameter] + public RenderFragment? Actions { get; set; } +} \ No newline at end of file diff --git a/src/Web/Insight.Web/Components/Dialogs/ChatDialog.razor b/src/Web/Insight.Web/Components/Dialogs/ChatDialog.razor new file mode 100644 index 0000000..5aeab56 --- /dev/null +++ b/src/Web/Insight.Web/Components/Dialogs/ChatDialog.razor @@ -0,0 +1,154 @@ +@using Insight.Web.Models.Account; + + + @if (_content == Content.Contacts) + { + + + + Chat + + + + + @* + # + contacts + *@ + + + + @foreach (var user in ChatService.Users.Keys.Where(p => p.Uid != SessionHandler.State.Uid).OrderByDescending(p => p.Online)) + { + +
+
+ + @* Content="3" *@ + + @user.Username?.ToUpper().FirstOrDefault() + + +
+
+ + @user.Username + + + @user.Uid + +
+
+
+ } +
+
+ } + else if (_content == Content.Chat && _currentSession is not null) + { + + + + Chat + + (@_currentSession.Members.FirstOrDefault(p => p.Uid != SessionHandler.State.Uid)?.Username) +
+ + @_currentSession?.Id.ToString() + +
+
+ + + + @foreach (var message in _currentSession.Messages.OrderBy(p => p.CreatedDate)) + { + if (message.SenderId == SessionHandler.State.Uid) + { + +
+
+ + @SessionHandler.State.Username?.ToUpper().FirstOrDefault() + +
+
+ + You + + + @message.CreatedDate.ToString("dd MMM yyyy - hh:mm tt") + + + @message.Message + +
+
+
+ } + else + { + var sender = ChatService.Users.Keys.FirstOrDefault(p => p.Uid == message.SenderId); + + +
+
+ + + @sender?.Username?.ToUpper().FirstOrDefault() + + +
+
+ + @sender?.Username + + + @message.CreatedDate.ToString("dd MMM yyyy - hh:mm tt") + + + @message.Message + +
+
+
+ } + } +
+ + + + + Send + + +
+ } +
+ +