Compare commits

..

No commits in common. "main" and "v1.3.0" have entirely different histories.
main ... v1.3.0

208 changed files with 302 additions and 24944 deletions

View file

@ -40,8 +40,6 @@ Warnings behandles som fejl, og .NET-analyzers kører på niveauet `latest-recom
5. Registrér reglen i `BuiltInRules`.
6. Tilføj `SQL Scripts/<id>.sql` med den danske header (hvad, vurdering, grænser, alvor, rettigheder, versioner, collector) og collectorens SQL, ordret og med en `-- Kilde:`-linje pr. variant. Brug en eksisterende fil som skabelon. `SqlScriptTests` fejler, hvis filen mangler, eller hvis SQL'en ikke længere er den, koden sender.
En regel om en Windows-server eller et syntetisk tjek (fra 1.5, `SQLI-WIN-…`/`SQLI-SYN-…`) implementerer `IScopedRule<TContext>` i `src/SQLInsight.Rules/Servers/` og registreres i `WindowsServerRules.Create()` eller `SyntheticCheckRules.Create()` (dermed i `ServerRuleCatalog`). Dens `SQL Scripts/<id>.sql` har samme header; forespørgslerne mod Windows står ordret i kommentarer og skal stå i `src/SQLInsight.Servers/CimQueries.cs` (`ServerScriptTests` tjekker det). Agenten må kun læse: CIM-`SELECT`, hændelsesloggen og skrivebeskyttede metoder som Task Schedulers `GetInfoByName`, aldrig start/stop eller ændringer.
### Uden C#: community-regler
Mange regler kan skrives som en SQL-forespørgsel i en JSON-fil under `rules/community/`. De kræver ingen C# og kan afprøves direkte mod en instans med `SQLInsight.Agent rules test`. Se [docs/regler.md](docs/regler.md). Også en community-regel skal have sin `SQL Scripts/<id>.sql` med forespørgslen ordret (se fx `SQL Scripts/COM-SEC-001.sql`).

View file

@ -14,7 +14,7 @@
<Product>SQLInsight Pro</Product>
<Copyright>Copyright (c) SEOSoft</Copyright>
<!-- Release version; set with -p:Version=… when publishing. Update checks compare against it. -->
<Version>1.5.2</Version>
<Version>1.3.0</Version>
</PropertyGroup>
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('Tests'))">

View file

@ -22,7 +22,6 @@
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.12" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="10.0.12" />
<PackageVersion Include="System.Diagnostics.EventLog" Version="10.0.12" />
<PackageVersion Include="Microsoft.Management.Infrastructure" Version="3.0.0" />
<PackageVersion Include="System.DirectoryServices.AccountManagement" Version="10.0.12" />
<PackageVersion Include="System.Security.Cryptography.ProtectedData" Version="10.0.12" />
<PackageVersion Include="PDFsharp-MigraDoc" Version="6.2.4" />

View file

@ -4,7 +4,7 @@
SQLInsight Pro er et overvågningsværktøj til Microsoft SQL Server, skrevet i C# på .NET. Det kører hos kunden og forlader ikke netværket, medmindre kunden selv vælger det. Værktøjet nøjes ikke med at vise tal. Det forklarer, hvad der er galt, hvorfor det er et problem, og hvordan det løses. Rådene har samme niveau som dem, man får fra en erfaren DBA.
> **Status:** Fase 1–4 er implementeret: agent, 108 indbyggede regler (92 for SQL Server; fra 1.5 også 13 for Windows-servere og 3 for syntetiske tjek) plus community-regler, notifikationer, lokalt repository og web-UI, licenser, signerede opdateringer, SQLInsight Hub med portal, erfaring på tværs af kunder, PDF-rapporter og integration med Prometheus, Grafana og SCOM, plus MSI-installer, community-websted og Discourse-forum. Se [installation](docs/installation.md), [opdateringer](docs/opdatering.md), [integration](docs/integration.md), [rapporter](docs/rapporter.md), [egne regler](docs/regler.md), [Dynamics AX](docs/dynamics-ax.md), [Windows-servere](docs/windows-servere.md), [syntetiske tjek](docs/syntetiske-tjek.md), [databehandling](docs/databehandling.md), [community](docs/community.md), [Roadmap](#roadmap) og [CONTRIBUTING.md](CONTRIBUTING.md).
> **Status:** Fase 1–4 er implementeret: agent, 82 indbyggede regler plus community-regler, notifikationer, lokalt repository og web-UI, licenser, signerede opdateringer, SQLInsight Hub med portal, erfaring på tværs af kunder, PDF-rapporter og integration med Prometheus, Grafana og SCOM, plus MSI-installer, community-websted og Discourse-forum. Se [installation](docs/installation.md), [opdateringer](docs/opdatering.md), [integration](docs/integration.md), [rapporter](docs/rapporter.md), [egne regler](docs/regler.md), [databehandling](docs/databehandling.md), [community](docs/community.md), [Roadmap](#roadmap) og [CONTRIBUTING.md](CONTRIBUTING.md).
---

View file

@ -5,9 +5,6 @@
-- Titel: Dynamics AX 3.0 er uden support fra Microsoft
-- Alvor: Medium
-- Versioner: SQL Server 2005 og nyere. Kræver ingen særlige capabilities (DeclarativeRule).
-- Kører kun i Dynamics AX-databaser (den fælles AX-genkendelse). Axapta 3.0: verificeret mod en kundes
-- skema; AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- Dynamics AX 3.0 er uden support fra Microsoft. Microsoft Dynamics AX 3.0 er for længst ude af Microsofts support,
@ -17,10 +14,8 @@
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- Ingen C#-logik ud over at læse resultatet: hver række, forespørgslen returnerer, er et fund (højst 100 rækker pr.
-- regel). Reglen er markeret "dynamicsAx" (fra 1.4): forespørgslen køres kun i de overvågede
-- databaser (CustomQueryCollector.DatabasesSql), som AX-genkendelsen (AxDatabaseDetector) genkender som Dynamics AX;
-- er der ingen, er reglen "ikke relevant". Fund går kun til SQLInsight Hub for databaser med AX-tilvalget.
-- Kolonnen database tilføjes hver række. Kører instansen en ældre version end minimumVersion, er reglen "ikke
-- regel). Forespørgslen køres i hver online brugerdatabase, agenten kan åbne (CustomQueryCollector.DatabasesSql),
-- og kolonnen database tilføjes hver række. Kører instansen en ældre version end minimumVersion, er reglen "ikke
-- relevant". Fejler forespørgslen pga. manglende rettigheder, er reglen "ikke målbar". Fundets titel: "{database}
-- er en Dynamics AX 3.0-database, og AX 3.0 er uden support" ({kolonne} udfyldes fra rækken).
--
@ -39,20 +34,15 @@
-- CustomQueryCollector.BuildRuleSql(database, query)
-- Interval: 15 min som standard (SQLInsight:CollectorIntervals:custom-rules)
-- Delt script: SQL Scripts/_collectors/CustomQueryCollector.sql
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- Regelkode: src/SQLInsight.Rules/Custom/DeclarativeRule.cs : DeclarativeRule.Evaluate; reglen selv:
-- rules/community/dynamics-ax.json (id COM-AX-001, indlejret i regelpakken)
--
-- Read-only: Ja: kun SELECT samt USE (skifter sessionens database), SET-sessionsindstillinger og AX-genkendelsen
-- via [db].sys.sp_executesql (kun metadata, ingen AX-tabeldata). Intet ændrer data, indstillinger eller
-- objekter.
-- Read-only: Ja: kun SELECT samt USE (skifter sessionens database), SET-sessionsindstillinger. Intet ændrer data,
-- indstillinger eller objekter.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); en variant for en anden SQL Server-version end din kan fejle - det er forventet.
-- Forespørgslerne i databasen er vist for AXDB: ret [AXDB] til din Dynamics AX-database.
-- Per-database-forespørgsler er vist for databasen msdb, som findes overalt: ret navnet til den
-- database, du vil se.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
@ -75,42 +65,10 @@ GO
SELECT name FROM sys.databases WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1 ORDER BY name
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke af databaserne er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- CustomQueryCollector: reglens forespørgsel i hver genkendt Dynamics AX-database
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: SQL Server 2005 og nyere
-- Kilde: CustomQueryCollector.BuildRuleSql
-- Kode: src/SQLInsight.Collectors/Custom/CustomQueryCollector.cs : CustomQueryCollector.BuildRuleSql(database, query)
-- Agenten kører dette i hver overvåget database, som AX-genkendelsen herover har genkendt. Ret [AXDB] til din AX-database.
USE [AXDB];
SELECT CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)) AS [sql_version] WHERE COLUMNPROPERTY((SELECT TOP 1 object_id FROM sys.tables WHERE name = N'SQLDICTIONARY'), N'RECID', 'Precision') = 10
-- Agenten kører dette i hver brugerdatabase fra listen ovenfor. Ret [msdb] til den database, du vil tjekke.
USE [msdb];
SELECT CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)) AS [sql_version] WHERE EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SQLDICTIONARY') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'DATAAREA') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SYSCONFIG') AND COLUMNPROPERTY((SELECT TOP 1 object_id FROM sys.tables WHERE name = N'SQLDICTIONARY'), N'RECID', 'Precision') = 10
GO

View file

@ -5,9 +5,6 @@
-- Titel: Dynamics AX 3.0 kører på en SQL Server-version, der ikke var understøttet til AX 3.0
-- Alvor: Medium
-- Versioner: SQL Server 2005 og nyere. Kræver ingen særlige capabilities (DeclarativeRule).
-- Kører kun i Dynamics AX-databaser (den fælles AX-genkendelse). Axapta 3.0: verificeret mod en kundes
-- skema; AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- Dynamics AX 3.0 kører på en SQL Server-version, der ikke var understøttet til AX 3.0. AX 3.0 blev kun frigivet
@ -18,10 +15,8 @@
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- Ingen C#-logik ud over at læse resultatet: hver række, forespørgslen returnerer, er et fund (højst 100 rækker pr.
-- regel). Reglen er markeret "dynamicsAx" (fra 1.4): forespørgslen køres kun i de overvågede
-- databaser (CustomQueryCollector.DatabasesSql), som AX-genkendelsen (AxDatabaseDetector) genkender som Dynamics AX;
-- er der ingen, er reglen "ikke relevant". Fund går kun til SQLInsight Hub for databaser med AX-tilvalget.
-- Kolonnen database tilføjes hver række. Kører instansen en ældre version end minimumVersion, er reglen "ikke
-- regel). Forespørgslen køres i hver online brugerdatabase, agenten kan åbne (CustomQueryCollector.DatabasesSql),
-- og kolonnen database tilføjes hver række. Kører instansen en ældre version end minimumVersion, er reglen "ikke
-- relevant". Fejler forespørgslen pga. manglende rettigheder, er reglen "ikke målbar". Fundets titel: "AX
-- 3.0-databasen {database} ligger på SQL Server {sql_version}" ({kolonne} udfyldes fra rækken).
--
@ -40,20 +35,15 @@
-- CustomQueryCollector.BuildRuleSql(database, query)
-- Interval: 15 min som standard (SQLInsight:CollectorIntervals:custom-rules)
-- Delt script: SQL Scripts/_collectors/CustomQueryCollector.sql
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- Regelkode: src/SQLInsight.Rules/Custom/DeclarativeRule.cs : DeclarativeRule.Evaluate; reglen selv:
-- rules/community/dynamics-ax.json (id COM-AX-002, indlejret i regelpakken)
--
-- Read-only: Ja: kun SELECT samt USE (skifter sessionens database), SET-sessionsindstillinger og AX-genkendelsen
-- via [db].sys.sp_executesql (kun metadata, ingen AX-tabeldata). Intet ændrer data, indstillinger eller
-- objekter.
-- Read-only: Ja: kun SELECT samt USE (skifter sessionens database), SET-sessionsindstillinger. Intet ændrer data,
-- indstillinger eller objekter.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); en variant for en anden SQL Server-version end din kan fejle - det er forventet.
-- Forespørgslerne i databasen er vist for AXDB: ret [AXDB] til din Dynamics AX-database.
-- Per-database-forespørgsler er vist for databasen msdb, som findes overalt: ret navnet til den
-- database, du vil se.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
@ -76,42 +66,10 @@ GO
SELECT name FROM sys.databases WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1 ORDER BY name
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke af databaserne er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- CustomQueryCollector: reglens forespørgsel i hver genkendt Dynamics AX-database
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: SQL Server 2005 og nyere
-- Kilde: CustomQueryCollector.BuildRuleSql
-- Kode: src/SQLInsight.Collectors/Custom/CustomQueryCollector.cs : CustomQueryCollector.BuildRuleSql(database, query)
-- Agenten kører dette i hver overvåget database, som AX-genkendelsen herover har genkendt. Ret [AXDB] til din AX-database.
USE [AXDB];
SELECT CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)) AS [sql_version], d.compatibility_level AS [compatibility_level] FROM sys.databases d WHERE d.database_id = DB_ID() AND CAST(PARSENAME(CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)), 4) AS int) > 9 AND COLUMNPROPERTY((SELECT TOP 1 object_id FROM sys.tables WHERE name = N'SQLDICTIONARY'), N'RECID', 'Precision') = 10
-- Agenten kører dette i hver brugerdatabase fra listen ovenfor. Ret [msdb] til den database, du vil tjekke.
USE [msdb];
SELECT CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)) AS [sql_version], d.compatibility_level AS [compatibility_level] FROM sys.databases d WHERE d.database_id = DB_ID() AND CAST(PARSENAME(CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)), 4) AS int) > 9 AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SQLDICTIONARY') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'DATAAREA') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SYSCONFIG') AND COLUMNPROPERTY((SELECT TOP 1 object_id FROM sys.tables WHERE name = N'SQLDICTIONARY'), N'RECID', 'Precision') = 10
GO

View file

@ -5,9 +5,6 @@
-- Titel: Dynamics AX-database på en instans, hvor MAXDOP ikke er 1 (SQL Server 2005-2014)
-- Alvor: Medium
-- Versioner: SQL Server 2005 og nyere. Kræver ingen særlige capabilities (DeclarativeRule).
-- Kører kun i Dynamics AX-databaser (den fælles AX-genkendelse). Axapta 3.0: verificeret mod en kundes
-- skema; AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- Dynamics AX-database på en instans, hvor MAXDOP ikke er 1 (SQL Server 2005-2014). Dynamics AX sender mange små,
@ -17,10 +14,8 @@
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- Ingen C#-logik ud over at læse resultatet: hver række, forespørgslen returnerer, er et fund (højst 100 rækker pr.
-- regel). Reglen er markeret "dynamicsAx" (fra 1.4): forespørgslen køres kun i de overvågede
-- databaser (CustomQueryCollector.DatabasesSql), som AX-genkendelsen (AxDatabaseDetector) genkender som Dynamics AX;
-- er der ingen, er reglen "ikke relevant". Fund går kun til SQLInsight Hub for databaser med AX-tilvalget.
-- Kolonnen database tilføjes hver række. Kører instansen en ældre version end minimumVersion, er reglen "ikke
-- regel). Forespørgslen køres i hver online brugerdatabase, agenten kan åbne (CustomQueryCollector.DatabasesSql),
-- og kolonnen database tilføjes hver række. Kører instansen en ældre version end minimumVersion, er reglen "ikke
-- relevant". Fejler forespørgslen pga. manglende rettigheder, er reglen "ikke målbar". Fundets titel: "MAXDOP er
-- {maxdop} på instansen med Dynamics AX-databasen {database}" ({kolonne} udfyldes fra rækken).
--
@ -39,20 +34,15 @@
-- CustomQueryCollector.BuildRuleSql(database, query)
-- Interval: 15 min som standard (SQLInsight:CollectorIntervals:custom-rules)
-- Delt script: SQL Scripts/_collectors/CustomQueryCollector.sql
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- Regelkode: src/SQLInsight.Rules/Custom/DeclarativeRule.cs : DeclarativeRule.Evaluate; reglen selv:
-- rules/community/dynamics-ax.json (id COM-AX-003, indlejret i regelpakken)
--
-- Read-only: Ja: kun SELECT samt USE (skifter sessionens database), SET-sessionsindstillinger og AX-genkendelsen
-- via [db].sys.sp_executesql (kun metadata, ingen AX-tabeldata). Intet ændrer data, indstillinger eller
-- objekter.
-- Read-only: Ja: kun SELECT samt USE (skifter sessionens database), SET-sessionsindstillinger. Intet ændrer data,
-- indstillinger eller objekter.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); en variant for en anden SQL Server-version end din kan fejle - det er forventet.
-- Forespørgslerne i databasen er vist for AXDB: ret [AXDB] til din Dynamics AX-database.
-- Per-database-forespørgsler er vist for databasen msdb, som findes overalt: ret navnet til den
-- database, du vil se.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
@ -75,42 +65,10 @@ GO
SELECT name FROM sys.databases WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1 ORDER BY name
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke af databaserne er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- CustomQueryCollector: reglens forespørgsel i hver genkendt Dynamics AX-database
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: SQL Server 2005 og nyere
-- Kilde: CustomQueryCollector.BuildRuleSql
-- Kode: src/SQLInsight.Collectors/Custom/CustomQueryCollector.cs : CustomQueryCollector.BuildRuleSql(database, query)
-- Agenten kører dette i hver overvåget database, som AX-genkendelsen herover har genkendt. Ret [AXDB] til din AX-database.
USE [AXDB];
SELECT CAST(c.value_in_use AS int) AS [maxdop] FROM sys.configurations c WHERE c.name = 'max degree of parallelism' AND CAST(c.value_in_use AS int) <> 1 AND CAST(PARSENAME(CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)), 4) AS int) < 13
-- Agenten kører dette i hver brugerdatabase fra listen ovenfor. Ret [msdb] til den database, du vil tjekke.
USE [msdb];
SELECT CAST(c.value_in_use AS int) AS [maxdop] FROM sys.configurations c WHERE c.name = 'max degree of parallelism' AND CAST(c.value_in_use AS int) <> 1 AND CAST(PARSENAME(CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)), 4) AS int) < 13 AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SQLDICTIONARY') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'DATAAREA') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SYSCONFIG')
GO

View file

@ -5,9 +5,6 @@
-- Titel: Dynamics AX-database, hvor MAXDOP ikke er 1 (SQL Server 2016 og nyere)
-- Alvor: Medium
-- Versioner: SQL Server 2016 og nyere. Kræver ingen særlige capabilities (DeclarativeRule).
-- Kører kun i Dynamics AX-databaser (den fælles AX-genkendelse). Axapta 3.0: verificeret mod en kundes
-- skema; AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- Dynamics AX-database, hvor MAXDOP ikke er 1 (SQL Server 2016 og nyere). Dynamics AX sender mange små, hyppige
@ -17,10 +14,8 @@
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- Ingen C#-logik ud over at læse resultatet: hver række, forespørgslen returnerer, er et fund (højst 100 rækker pr.
-- regel). Reglen er markeret "dynamicsAx" (fra 1.4): forespørgslen køres kun i de overvågede
-- databaser (CustomQueryCollector.DatabasesSql), som AX-genkendelsen (AxDatabaseDetector) genkender som Dynamics AX;
-- er der ingen, er reglen "ikke relevant". Fund går kun til SQLInsight Hub for databaser med AX-tilvalget.
-- Kolonnen database tilføjes hver række. Kører instansen en ældre version end minimumVersion, er reglen "ikke
-- regel). Forespørgslen køres i hver online brugerdatabase, agenten kan åbne (CustomQueryCollector.DatabasesSql),
-- og kolonnen database tilføjes hver række. Kører instansen en ældre version end minimumVersion, er reglen "ikke
-- relevant". Fejler forespørgslen pga. manglende rettigheder, er reglen "ikke målbar". Fundets titel: "Dynamics
-- AX-databasen {database} kører med MAXDOP større end 1" ({kolonne} udfyldes fra rækken).
--
@ -39,20 +34,15 @@
-- CustomQueryCollector.BuildRuleSql(database, query)
-- Interval: 15 min som standard (SQLInsight:CollectorIntervals:custom-rules)
-- Delt script: SQL Scripts/_collectors/CustomQueryCollector.sql
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- Regelkode: src/SQLInsight.Rules/Custom/DeclarativeRule.cs : DeclarativeRule.Evaluate; reglen selv:
-- rules/community/dynamics-ax.json (id COM-AX-004, indlejret i regelpakken)
--
-- Read-only: Ja: kun SELECT samt USE (skifter sessionens database), SET-sessionsindstillinger og AX-genkendelsen
-- via [db].sys.sp_executesql (kun metadata, ingen AX-tabeldata). Intet ændrer data, indstillinger eller
-- objekter.
-- Read-only: Ja: kun SELECT samt USE (skifter sessionens database), SET-sessionsindstillinger. Intet ændrer data,
-- indstillinger eller objekter.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); en variant for en anden SQL Server-version end din kan fejle - det er forventet.
-- Forespørgslerne i databasen er vist for AXDB: ret [AXDB] til din Dynamics AX-database.
-- Per-database-forespørgsler er vist for databasen msdb, som findes overalt: ret navnet til den
-- database, du vil se.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
@ -75,42 +65,10 @@ GO
SELECT name FROM sys.databases WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1 ORDER BY name
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke af databaserne er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- CustomQueryCollector: reglens forespørgsel i hver genkendt Dynamics AX-database
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: SQL Server 2016 og nyere
-- Kilde: CustomQueryCollector.BuildRuleSql
-- Kode: src/SQLInsight.Collectors/Custom/CustomQueryCollector.cs : CustomQueryCollector.BuildRuleSql(database, query)
-- Agenten kører dette i hver overvåget database, som AX-genkendelsen herover har genkendt. Ret [AXDB] til din AX-database.
USE [AXDB];
SELECT CAST(c.value_in_use AS int) AS [server_maxdop], CAST(s.value AS int) AS [database_maxdop] FROM sys.configurations c CROSS JOIN sys.database_scoped_configurations s WHERE c.name = 'max degree of parallelism' AND s.name = 'MAXDOP' AND (CASE WHEN CAST(s.value AS int) = 0 THEN CAST(c.value_in_use AS int) ELSE CAST(s.value AS int) END) <> 1
-- Agenten kører dette i hver brugerdatabase fra listen ovenfor. Ret [msdb] til den database, du vil tjekke.
USE [msdb];
SELECT CAST(c.value_in_use AS int) AS [server_maxdop], CAST(s.value AS int) AS [database_maxdop] FROM sys.configurations c CROSS JOIN sys.database_scoped_configurations s WHERE c.name = 'max degree of parallelism' AND s.name = 'MAXDOP' AND (CASE WHEN CAST(s.value AS int) = 0 THEN CAST(c.value_in_use AS int) ELSE CAST(s.value AS int) END) <> 1 AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SQLDICTIONARY') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'DATAAREA') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SYSCONFIG')
GO

View file

@ -5,9 +5,6 @@
-- Titel: Dynamics AX-tabeller uden unikt RecId-indeks
-- Alvor: Medium
-- Versioner: SQL Server 2005 og nyere. Kræver ingen særlige capabilities (DeclarativeRule).
-- Kører kun i Dynamics AX-databaser (den fælles AX-genkendelse). Axapta 3.0: verificeret mod en kundes
-- skema; AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- Dynamics AX-tabeller uden unikt RecId-indeks. AX identificerer hver post med RecId og opretter normalt et unikt
@ -17,10 +14,8 @@
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- Ingen C#-logik ud over at læse resultatet: hver række, forespørgslen returnerer, er et fund (højst 100 rækker pr.
-- regel). Reglen er markeret "dynamicsAx" (fra 1.4): forespørgslen køres kun i de overvågede
-- databaser (CustomQueryCollector.DatabasesSql), som AX-genkendelsen (AxDatabaseDetector) genkender som Dynamics AX;
-- er der ingen, er reglen "ikke relevant". Fund går kun til SQLInsight Hub for databaser med AX-tilvalget.
-- Kolonnen database tilføjes hver række. Kører instansen en ældre version end minimumVersion, er reglen "ikke
-- regel). Forespørgslen køres i hver online brugerdatabase, agenten kan åbne (CustomQueryCollector.DatabasesSql),
-- og kolonnen database tilføjes hver række. Kører instansen en ældre version end minimumVersion, er reglen "ikke
-- relevant". Fejler forespørgslen pga. manglende rettigheder, er reglen "ikke målbar". Fundets titel: "{tables}
-- AX-tabeller i {database} har intet unikt indeks på RECID" ({kolonne} udfyldes fra rækken).
--
@ -39,20 +34,15 @@
-- CustomQueryCollector.BuildRuleSql(database, query)
-- Interval: 15 min som standard (SQLInsight:CollectorIntervals:custom-rules)
-- Delt script: SQL Scripts/_collectors/CustomQueryCollector.sql
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- Regelkode: src/SQLInsight.Rules/Custom/DeclarativeRule.cs : DeclarativeRule.Evaluate; reglen selv:
-- rules/community/dynamics-ax.json (id COM-AX-005, indlejret i regelpakken)
--
-- Read-only: Ja: kun SELECT samt USE (skifter sessionens database), SET-sessionsindstillinger og AX-genkendelsen
-- via [db].sys.sp_executesql (kun metadata, ingen AX-tabeldata). Intet ændrer data, indstillinger eller
-- objekter.
-- Read-only: Ja: kun SELECT samt USE (skifter sessionens database), SET-sessionsindstillinger. Intet ændrer data,
-- indstillinger eller objekter.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); en variant for en anden SQL Server-version end din kan fejle - det er forventet.
-- Forespørgslerne i databasen er vist for AXDB: ret [AXDB] til din Dynamics AX-database.
-- Per-database-forespørgsler er vist for databasen msdb, som findes overalt: ret navnet til den
-- database, du vil se.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
@ -75,42 +65,10 @@ GO
SELECT name FROM sys.databases WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1 ORDER BY name
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke af databaserne er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- CustomQueryCollector: reglens forespørgsel i hver genkendt Dynamics AX-database
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: SQL Server 2005 og nyere
-- Kilde: CustomQueryCollector.BuildRuleSql
-- Kode: src/SQLInsight.Collectors/Custom/CustomQueryCollector.cs : CustomQueryCollector.BuildRuleSql(database, query)
-- Agenten kører dette i hver overvåget database, som AX-genkendelsen herover har genkendt. Ret [AXDB] til din AX-database.
USE [AXDB];
SELECT COUNT(*) AS [tables], LEFT(STUFF((SELECT TOP 10 N', ' + t2.name FROM sys.tables t2 WHERE COLUMNPROPERTY(t2.object_id, N'RECID', 'ColumnId') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys.indexes i JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.is_included_column = 0 JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id WHERE i.object_id = t2.object_id AND i.is_unique = 1 AND c.name = N'RECID') ORDER BY t2.name FOR XML PATH('')), 1, 2, ''), 400) AS [examples] FROM sys.tables t WHERE COLUMNPROPERTY(t.object_id, N'RECID', 'ColumnId') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys.indexes i JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.is_included_column = 0 JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id WHERE i.object_id = t.object_id AND i.is_unique = 1 AND c.name = N'RECID') HAVING COUNT(*) > 0
-- Agenten kører dette i hver brugerdatabase fra listen ovenfor. Ret [msdb] til den database, du vil tjekke.
USE [msdb];
SELECT COUNT(*) AS [tables], LEFT(STUFF((SELECT TOP 10 N', ' + t2.name FROM sys.tables t2 WHERE COLUMNPROPERTY(t2.object_id, N'RECID', 'ColumnId') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys.indexes i JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.is_included_column = 0 JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id WHERE i.object_id = t2.object_id AND i.is_unique = 1 AND c.name = N'RECID') ORDER BY t2.name FOR XML PATH('')), 1, 2, ''), 400) AS [examples] FROM sys.tables t WHERE COLUMNPROPERTY(t.object_id, N'RECID', 'ColumnId') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys.indexes i JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.is_included_column = 0 JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id WHERE i.object_id = t.object_id AND i.is_unique = 1 AND c.name = N'RECID') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SQLDICTIONARY') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'DATAAREA') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SYSCONFIG') HAVING COUNT(*) > 0
GO

View file

@ -5,9 +5,6 @@
-- Titel: Store Dynamics AX-tabeller uden clustered index (heaps)
-- Alvor: Medium
-- Versioner: SQL Server 2005 og nyere. Kræver ingen særlige capabilities (DeclarativeRule).
-- Kører kun i Dynamics AX-databaser (den fælles AX-genkendelse). Axapta 3.0: verificeret mod en kundes
-- skema; AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- Store Dynamics AX-tabeller uden clustered index (heaps). En tabel uden clustered index (en heap) får spredte data
@ -17,10 +14,8 @@
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- Ingen C#-logik ud over at læse resultatet: hver række, forespørgslen returnerer, er et fund (højst 100 rækker pr.
-- regel). Reglen er markeret "dynamicsAx" (fra 1.4): forespørgslen køres kun i de overvågede
-- databaser (CustomQueryCollector.DatabasesSql), som AX-genkendelsen (AxDatabaseDetector) genkender som Dynamics AX;
-- er der ingen, er reglen "ikke relevant". Fund går kun til SQLInsight Hub for databaser med AX-tilvalget.
-- Kolonnen database tilføjes hver række. Kører instansen en ældre version end minimumVersion, er reglen "ikke
-- regel). Forespørgslen køres i hver online brugerdatabase, agenten kan åbne (CustomQueryCollector.DatabasesSql),
-- og kolonnen database tilføjes hver række. Kører instansen en ældre version end minimumVersion, er reglen "ikke
-- relevant". Fejler forespørgslen pga. manglende rettigheder, er reglen "ikke målbar". Fundets titel: "{tables}
-- store AX-tabeller i {database} har intet clustered index" ({kolonne} udfyldes fra rækken).
--
@ -39,20 +34,15 @@
-- CustomQueryCollector.BuildRuleSql(database, query)
-- Interval: 15 min som standard (SQLInsight:CollectorIntervals:custom-rules)
-- Delt script: SQL Scripts/_collectors/CustomQueryCollector.sql
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- Regelkode: src/SQLInsight.Rules/Custom/DeclarativeRule.cs : DeclarativeRule.Evaluate; reglen selv:
-- rules/community/dynamics-ax.json (id COM-AX-006, indlejret i regelpakken)
--
-- Read-only: Ja: kun SELECT samt USE (skifter sessionens database), SET-sessionsindstillinger og AX-genkendelsen
-- via [db].sys.sp_executesql (kun metadata, ingen AX-tabeldata). Intet ændrer data, indstillinger eller
-- objekter.
-- Read-only: Ja: kun SELECT samt USE (skifter sessionens database), SET-sessionsindstillinger. Intet ændrer data,
-- indstillinger eller objekter.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); en variant for en anden SQL Server-version end din kan fejle - det er forventet.
-- Forespørgslerne i databasen er vist for AXDB: ret [AXDB] til din Dynamics AX-database.
-- Per-database-forespørgsler er vist for databasen msdb, som findes overalt: ret navnet til den
-- database, du vil se.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
@ -75,42 +65,10 @@ GO
SELECT name FROM sys.databases WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1 ORDER BY name
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke af databaserne er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- CustomQueryCollector: reglens forespørgsel i hver genkendt Dynamics AX-database
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: SQL Server 2005 og nyere
-- Kilde: CustomQueryCollector.BuildRuleSql
-- Kode: src/SQLInsight.Collectors/Custom/CustomQueryCollector.cs : CustomQueryCollector.BuildRuleSql(database, query)
-- Agenten kører dette i hver overvåget database, som AX-genkendelsen herover har genkendt. Ret [AXDB] til din AX-database.
USE [AXDB];
SELECT COUNT(*) AS [tables], SUM(h.row_count) AS [rows], LEFT(STUFF((SELECT TOP 10 N', ' + OBJECT_NAME(h2.object_id) + N' (' + CAST(h2.row_count AS nvarchar(20)) + N')' FROM (SELECT p.object_id, SUM(p.rows) AS row_count FROM sys.partitions p JOIN sys.tables t ON t.object_id = p.object_id WHERE p.index_id = 0 GROUP BY p.object_id) h2 WHERE h2.row_count >= 100000 ORDER BY h2.row_count DESC FOR XML PATH('')), 1, 2, ''), 400) AS [largest] FROM (SELECT p.object_id, SUM(p.rows) AS row_count FROM sys.partitions p JOIN sys.tables t ON t.object_id = p.object_id WHERE p.index_id = 0 GROUP BY p.object_id) h WHERE h.row_count >= 100000 HAVING COUNT(*) > 0
-- Agenten kører dette i hver brugerdatabase fra listen ovenfor. Ret [msdb] til den database, du vil tjekke.
USE [msdb];
SELECT COUNT(*) AS [tables], SUM(h.row_count) AS [rows], LEFT(STUFF((SELECT TOP 10 N', ' + OBJECT_NAME(h2.object_id) + N' (' + CAST(h2.row_count AS nvarchar(20)) + N')' FROM (SELECT p.object_id, SUM(p.rows) AS row_count FROM sys.partitions p JOIN sys.tables t ON t.object_id = p.object_id WHERE p.index_id = 0 GROUP BY p.object_id) h2 WHERE h2.row_count >= 100000 ORDER BY h2.row_count DESC FOR XML PATH('')), 1, 2, ''), 400) AS [largest] FROM (SELECT p.object_id, SUM(p.rows) AS row_count FROM sys.partitions p JOIN sys.tables t ON t.object_id = p.object_id WHERE p.index_id = 0 GROUP BY p.object_id) h WHERE h.row_count >= 100000 AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SQLDICTIONARY') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'DATAAREA') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SYSCONFIG') HAVING COUNT(*) > 0
GO

View file

@ -5,9 +5,6 @@
-- Titel: Automatiske statistikker er slået fra i en Dynamics AX-database
-- Alvor: High
-- Versioner: SQL Server 2005 og nyere. Kræver ingen særlige capabilities (DeclarativeRule).
-- Kører kun i Dynamics AX-databaser (den fælles AX-genkendelse). Axapta 3.0: verificeret mod en kundes
-- skema; AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- Automatiske statistikker er slået fra i en Dynamics AX-database. AX bygger sine forespørgsler dynamisk ud fra
@ -18,10 +15,8 @@
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- Ingen C#-logik ud over at læse resultatet: hver række, forespørgslen returnerer, er et fund (højst 100 rækker pr.
-- regel). Reglen er markeret "dynamicsAx" (fra 1.4): forespørgslen køres kun i de overvågede
-- databaser (CustomQueryCollector.DatabasesSql), som AX-genkendelsen (AxDatabaseDetector) genkender som Dynamics AX;
-- er der ingen, er reglen "ikke relevant". Fund går kun til SQLInsight Hub for databaser med AX-tilvalget.
-- Kolonnen database tilføjes hver række. Kører instansen en ældre version end minimumVersion, er reglen "ikke
-- regel). Forespørgslen køres i hver online brugerdatabase, agenten kan åbne (CustomQueryCollector.DatabasesSql),
-- og kolonnen database tilføjes hver række. Kører instansen en ældre version end minimumVersion, er reglen "ikke
-- relevant". Fejler forespørgslen pga. manglende rettigheder, er reglen "ikke målbar". Fundets titel: "Automatiske
-- statistikker er slået fra i AX-databasen {database}" ({kolonne} udfyldes fra rækken).
--
@ -40,20 +35,15 @@
-- CustomQueryCollector.BuildRuleSql(database, query)
-- Interval: 15 min som standard (SQLInsight:CollectorIntervals:custom-rules)
-- Delt script: SQL Scripts/_collectors/CustomQueryCollector.sql
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- Regelkode: src/SQLInsight.Rules/Custom/DeclarativeRule.cs : DeclarativeRule.Evaluate; reglen selv:
-- rules/community/dynamics-ax.json (id COM-AX-007, indlejret i regelpakken)
--
-- Read-only: Ja: kun SELECT samt USE (skifter sessionens database), SET-sessionsindstillinger og AX-genkendelsen
-- via [db].sys.sp_executesql (kun metadata, ingen AX-tabeldata). Intet ændrer data, indstillinger eller
-- objekter.
-- Read-only: Ja: kun SELECT samt USE (skifter sessionens database), SET-sessionsindstillinger. Intet ændrer data,
-- indstillinger eller objekter.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); en variant for en anden SQL Server-version end din kan fejle - det er forventet.
-- Forespørgslerne i databasen er vist for AXDB: ret [AXDB] til din Dynamics AX-database.
-- Per-database-forespørgsler er vist for databasen msdb, som findes overalt: ret navnet til den
-- database, du vil se.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
@ -76,42 +66,10 @@ GO
SELECT name FROM sys.databases WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1 ORDER BY name
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke af databaserne er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- CustomQueryCollector: reglens forespørgsel i hver genkendt Dynamics AX-database
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: SQL Server 2005 og nyere
-- Kilde: CustomQueryCollector.BuildRuleSql
-- Kode: src/SQLInsight.Collectors/Custom/CustomQueryCollector.cs : CustomQueryCollector.BuildRuleSql(database, query)
-- Agenten kører dette i hver overvåget database, som AX-genkendelsen herover har genkendt. Ret [AXDB] til din AX-database.
USE [AXDB];
SELECT CASE WHEN d.is_auto_create_stats_on = 1 THEN 'til' ELSE 'fra' END AS [auto_create_statistics], CASE WHEN d.is_auto_update_stats_on = 1 THEN 'til' ELSE 'fra' END AS [auto_update_statistics] FROM sys.databases d WHERE d.database_id = DB_ID() AND (d.is_auto_create_stats_on = 0 OR d.is_auto_update_stats_on = 0)
-- Agenten kører dette i hver brugerdatabase fra listen ovenfor. Ret [msdb] til den database, du vil tjekke.
USE [msdb];
SELECT CASE WHEN d.is_auto_create_stats_on = 1 THEN 'til' ELSE 'fra' END AS [auto_create_statistics], CASE WHEN d.is_auto_update_stats_on = 1 THEN 'til' ELSE 'fra' END AS [auto_update_statistics] FROM sys.databases d WHERE d.database_id = DB_ID() AND (d.is_auto_create_stats_on = 0 OR d.is_auto_update_stats_on = 0) AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SQLDICTIONARY') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'DATAAREA') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SYSCONFIG')
GO

View file

@ -7,10 +7,6 @@ rettigheder, på hvilke versioner, og hvordan reglen bagefter vurderer data.
Agenten kører **ikke** filerne her. De er en læsbar kopi af SQL'en i koden, og unit tests sikrer, at kopien er
identisk med koden (se [Hold mappen i sync](#hold-mappen-i-sync)).
Fra 1.5 har også reglerne for Windows-servere (`SQLI-WIN-…`) og syntetiske tjek (`SQLI-SYN-…`) en fil her, med samme
header. De fleste af dem sender ingen SQL: forespørgslerne mod Windows (CIM/WQL og hændelseslogsfilteret) står ordret i
kommentarer, så de kan gennemgås på samme måde (se [Windows-servere og syntetiske tjek](#windows-servere-og-syntetiske-tjek-fra-15)).
## Navngivning
| Fil | Indhold |
@ -18,9 +14,7 @@ kommentarer, så de kan gennemgås på samme måde (se [Windows-servere og synte
| `<regel-id>.sql` | Én fil pr. regel, navngivet med regel-id'et præcis som i systemet: `SQLI-…` for de indbyggede regler (C#), `COM-…` for community-regler (JSON i `rules/community/`). |
| `_collectors/<Collector>.sql` | Alle forespørgsler fra én collector. Flere regler deler ofte samme collector; regelfilerne gentager SQL'en, så hver fil kan køres for sig. |
| `_collectors/InstanceProbe.sql` | Registreringen af instansen (version, udgave, Always On) og de sessionsindstillinger, agenten sætter på hver forbindelse. |
| `_collectors/AxDatabaseDetector.sql`, `_collectors/AxBatchCollector.sql`, `_collectors/AxSequenceCollector.sql` | Dynamics AX (fra 1.4): den fælles AX-genkendelse og læsningen af batchjob, nummerserier og RecId i AX-databasen. AX-forespørgslerne er vist for hver AX-version, SQLInsight har et skema for (Axapta 3.0, AX 4.0, AX 2009 og AX 2012), og for databasen `AXDB`. |
| `_collectors/DatabaseDiscovery.sql`, `_collectors/PermissionCheck.sql` | Søgningen efter databaser (med Dynamics AX-genkendelse) og forbindelsestesten med rettighedstjek under *Servere og databaser* (fra 1.3). De køres kun, når der bedes om dem i webbrugerfladen, og søgningen desuden hver time. |
| `_collectors/SyntheticCheckRunner.sql` | Det syntetiske SQL-tjek (fra 1.5): sessionsindstillingerne, `USE`, transaktionen og standardforespørgslen. Tjekkets egen forespørgsel køres i stedet for `SELECT 1`, og transaktionen rulles altid tilbage. |
## Opbygning af en regelfil
@ -67,9 +61,10 @@ Bemærk:
- **Read-only.** Alle scripts læser kun: SELECT mod katalog-views og DMV'er, `DBCC TRACESTATUS`, `DBCC SQLPERF(WAITSTATS)`
(kun SQL Server 2000), `xp_readerrorlog`, `xp_msver`, `sys.fn_trace_gettable`, lokale variabler og `USE`.
Ingen undtagelser: heller ikke midlertidige tabeller i tempdb. Dynamics AX-læsningerne (fra 1.4) kører som
`EXEC [AXDB].sys.sp_executesql N'…'`, så sessionen aldrig skifter database; batchen i strengen er selv kun SELECT med
`TOP` (READ COMMITTED med `SET LOCK_TIMEOUT 2000`, eller SNAPSHOT, aldrig NOLOCK), og testen tjekker den på samme måde.
**Én undtagelse:** `SQLI-DLK-001` / `_collectors/DeadlockCollector.sql` (SQL Server 2012+) opretter en midlertidig
tabel `#system_health` i tempdb og sletter den igen i samme batch. Den er privat for sessionen, og ingen brugerdata,
indstillinger eller permanente objekter ændres, men det er strengt taget en skrivning i tempdb. Det står også i
filernes header.
- **Per-database-forespørgsler** (indexes, statistikker, Query Store, database-scoped community-regler) kører agenten i
hver online brugerdatabase. Her er de vist for `msdb`, som findes på alle instanser, så scriptet kan køres uændret.
Ret `[msdb]` / `N'msdb'` til den database, du vil se.
@ -80,48 +75,26 @@ Bemærk:
(fx baseline-historikken bag `SQLI-ANOM-001`, som ikke hentes fra den overvågede server) og kundens lokale regler
(`LOCAL-…`).
## Windows-servere og syntetiske tjek (fra 1.5)
- **`SQLI-WIN-…`** sender ingen SQL. Filen har samme header som de andre regler (rettighederne er dem, som
`db/permissions/grant-windows-server.ps1` giver agentens konto, aldrig lokal administrator) og viser derefter, i
kommentarer, de præcise CIM/WQL-forespørgsler (kun `SELECT`), XPath-filteret til hændelsesloggen og det eneste
metodekald, Task Schedulers skrivebeskyttede `GetInfoByName`, samt PowerShell til at efterprøve dem. Agenten starter,
stopper eller ændrer intet på serveren. Filerne kan åbnes i SSMS, men gør intet.
- **`SQLI-SYN-001` og `SQLI-SYN-002`** viser præcis det, et syntetisk SQL-tjek sender: sessionsindstillingerne, `USE`
(kun med en database), `BEGIN TRANSACTION`, forespørgslen (standard `SELECT 1`) og `IF @@TRANCOUNT > 0 ROLLBACK
TRANSACTION`, der altid køres bagefter. En egen forespørgsel skal være én skrivebeskyttet SELECT
(`src/SQLInsight.Core/CustomRules/ReadOnlyQuery.cs`). TCP-, HTTP-, DNS- og AX-tjek sender ingen SQL.
- **`SQLI-SYN-003`** (TLS-certifikatet) sender ingen SQL.
## Hold mappen i sync
`tests/SQLInsight.Collectors.Tests/SqlScripts/SqlScriptTests.cs` fejler, hvis mappen og koden ikke stemmer overens:
- `Every_registered_rule_has_a_script_and_every_script_a_rule`: hver indbygget regel (`BuiltInRules` og, fra 1.5,
`ServerRuleCatalog`) og hver community-regel i regelpakken har en `<id>.sql`, og der er ingen filer for ukendte id'er.
- `Every_registered_rule_has_a_script_and_every_script_a_rule`: hver indbygget regel (`BuiltInRules`) og hver
community-regel i regelpakken har en `<id>.sql`, og der er ingen filer for ukendte id'er.
- `Scripts_contain_the_current_sql_from_the_code`: hver blok med `-- Kilde: <nøgle>` indeholder SQL'en, som koden
bygger den i dag, ordret (nøglerne står i `SqlScriptSources.cs`). Kun værdier, agenten beregner ved kørsel, må stå
anderledes. Kun Windows-server- og syntetiske-tjek-reglerne må være uden SQL-blokke.
anderledes.
- `Every_collector_query_is_registered_and_in_a_collector_script`: hver SQL-konstant og hver `Build…Sql`-metode i
collectorerne er registreret, og hver registreret forespørgsel står i et `_collectors/`-script.
- `Community_rule_scripts_contain_the_rule_query_verbatim`: community-reglens `query` fra JSON står ordret i filen.
- `Scripts_are_read_only`: ingen skrivende sætninger, heller ikke `SELECT … INTO #temp` - heller ikke i batchen i en
`EXEC [db].sys.sp_executesql N'…'`.
- `tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs` (Dynamics AX): hver AX-regel og AX-collector viser SQL'en for alle
AX-versioner, og headeren siger, hvad der ikke er verificeret mod kundedata.
- `tests/SQLInsight.Servers.Tests/ServerScriptTests.cs` (fra 1.5): hver CIM/WQL-forespørgsel og hvert
hændelseslogsfilter i `src/SQLInsight.Servers/CimQueries.cs` står ordret i en `SQLI-WIN-…`-fil, og hver af filerne viser
mindst én af dem. `tests/SQLInsight.Rules.Tests/ServerRuleScriptTests.cs`: regel-id og titel i headeren er reglens.
Er en collectors SQL ændret, kan blokkene skrives om fra koden: kør testene med `SQLINSIGHT_UPDATE_SQL_SCRIPTS=1`
(kommentarlinjerne bevares). Blokke med værdier, agenten beregner ved kørsel (⟪…⟫), udfyldes kun, når de er nye; værdien så sættes ind i hånden.
- `Scripts_are_read_only_apart_from_the_documented_temp_table`: ingen skrivende sætninger ud over undtagelsen ovenfor.
En ny regel skal derfor have sin `SQL Scripts/<id>.sql` (se `CONTRIBUTING.md`). Ændres en collectors SQL, peger
testen på de filer, der skal opdateres.
## Regler
124 regler: 108 indbyggede (C#: 92 for SQL Server, 13 for Windows-servere og 3 for syntetiske tjek, fra 1.5) og 16 community-regler (JSON).
98 regler: 82 indbyggede (C#) og 16 community-regler (JSON).
| Regel-id | Fil | Titel | Collector(s) | Alvor |
|---|---|---|---|---|
@ -129,16 +102,6 @@ testen på de filer, der skal opdateres.
| SQLI-AG-002 | [SQLI-AG-002.sql](SQLI-AG-002.sql) | Sekundær replika er langt bagud | AvailabilityGroupCollector | Medium |
| SQLI-AG-003 | [SQLI-AG-003.sql](SQLI-AG-003.sql) | En node eller et vidne i clusteret er nede | AvailabilityGroupCollector | High |
| SQLI-ANOM-001 | [SQLI-ANOM-001.sql](SQLI-ANOM-001.sql) | Usædvanlig belastning i forhold til det normale | CpuCollector, PerformanceCounterCollector | CPU: Medium. Batch requests: Low ved stigning, Medium ved fald. Page life expectancy: Medium. |
| SQLI-AX-001 | [SQLI-AX-001.sql](SQLI-AX-001.sql) | Dynamics AX-batchjob er endt med fejl | AxDatabaseDetector, AxBatchCollector | High |
| SQLI-AX-002 | [SQLI-AX-002.sql](SQLI-AX-002.sql) | Dynamics AX-batchjob kører længere end normalt | AxDatabaseDetector, AxBatchCollector | Medium; High, når kørslen er dobbelt så lang som grænsen. |
| SQLI-AX-003 | [SQLI-AX-003.sql](SQLI-AX-003.sql) | Dynamics AX-batchjob er ikke startet til tiden | AxDatabaseDetector, AxBatchCollector | Medium; High, når jobbet i praksis aldrig er startet. |
| SQLI-AX-004 | [SQLI-AX-004.sql](SQLI-AX-004.sql) | Dynamics AX-batchjob hænger i Udfører | AxDatabaseDetector, AxBatchCollector | High |
| SQLI-AX-005 | [SQLI-AX-005.sql](SQLI-AX-005.sql) | Dynamics AX-nummerserie er ved at løbe tør | AxDatabaseDetector, AxSequenceCollector | Medium; High over den kritiske procent eller inden for de kritiske dage; Critical, når serien er opbrugt. |
| SQLI-AX-006 | [SQLI-AX-006.sql](SQLI-AX-006.sql) | Dynamics AX-nummerseriens format er for kort til Højeste | AxDatabaseDetector, AxSequenceCollector | Medium; High, når NEXTREC allerede er større end formatets kapacitet. |
| SQLI-AX-007 | [SQLI-AX-007.sql](SQLI-AX-007.sql) | Spærret Dynamics AX-nummerserie bruges stadig | AxDatabaseDetector, AxSequenceCollector | Medium |
| SQLI-AX-008 | [SQLI-AX-008.sql](SQLI-AX-008.sql) | Kontinuerlig Dynamics AX-nummerserie er ikke ryddet op | AxDatabaseDetector, AxSequenceCollector | Low |
| SQLI-AX-009 | [SQLI-AX-009.sql](SQLI-AX-009.sql) | Dynamics AX RecId er ved at løbe tør | AxDatabaseDetector, AxSequenceCollector | Axapta 3.0: High/Critical efter grænserne; 64-bit: High; Critical, når RecId er opbrugt. |
| SQLI-AX-010 | [SQLI-AX-010.sql](SQLI-AX-010.sql) | AX-bruger blokerer andre | AxDatabaseDetector, BlockingCollector | Medium; High ved ventetid over 5 minutter eller mindst 10 blokerede sessioner. |
| SQLI-BLK-001 | [SQLI-BLK-001.sql](SQLI-BLK-001.sql) | Langvarig blocking | BlockingCollector | Medium; High når længste ventetid er mindst 5 minutter eller mindst 10 sessioner er blokeret. |
| SQLI-CFG-001 | [SQLI-CFG-001.sql](SQLI-CFG-001.sql) | max server memory er ikke sat | ConfigurationCollector, ServerCollector | High |
| SQLI-CFG-002 | [SQLI-CFG-002.sql](SQLI-CFG-002.sql) | max server memory efterlader for lidt til Windows | ConfigurationCollector, ServerCollector | Medium |
@ -203,9 +166,6 @@ testen på de filer, der skal opdateres.
| SQLI-STAT-001 | [SQLI-STAT-001.sql](SQLI-STAT-001.sql) | Forældede statistikker på store tabeller | IndexCollector | Low |
| SQLI-STAT-002 | [SQLI-STAT-002.sql](SQLI-STAT-002.sql) | Automatisk statistik er slået fra | DatabaseCollector | Medium |
| SQLI-SVC-001 | [SQLI-SVC-001.sql](SQLI-SVC-001.sql) | En SQL Server-tjeneste, der skal starte automatisk, kører ikke | InstanceConfigurationCollector | High |
| SQLI-SYN-001 | [SQLI-SYN-001.sql](SQLI-SYN-001.sql) | Et syntetisk tjek fejler | SyntheticCheckRunner | High. |
| SQLI-SYN-002 | [SQLI-SYN-002.sql](SQLI-SYN-002.sql) | Et syntetisk tjek svarer langsomt | SyntheticCheckRunner | Medium. |
| SQLI-SYN-003 | [SQLI-SYN-003.sql](SQLI-SYN-003.sql) | TLS-certifikatet udløber snart | SyntheticCheckRunner (HTTPS, ingen SQL) | Medium; High inden for CertificateCriticalDays; Critical når det er udløbet. |
| SQLI-TEMP-001 | [SQLI-TEMP-001.sql](SQLI-TEMP-001.sql) | Version store i tempdb er stor eller vokser | TempdbCollector | Medium |
| SQLI-TEMP-002 | [SQLI-TEMP-002.sql](SQLI-TEMP-002.sql) | tempdb er næsten fuld | TempdbCollector | High |
| SQLI-TEMP-003 | [SQLI-TEMP-003.sql](SQLI-TEMP-003.sql) | Session bruger meget tempdb i lang tid | TempdbCollector | Medium |
@ -220,19 +180,6 @@ testen på de filer, der skal opdateres.
| SQLI-WAIT-005 | [SQLI-WAIT-005.sql](SQLI-WAIT-005.sql) | Commits venter på langsom skrivning til transaktionsloggen | WaitStatsCollector | Medium; High fra 15 ms i gennemsnit. |
| SQLI-WAIT-006 | [SQLI-WAIT-006.sql](SQLI-WAIT-006.sql) | Parallelle forespørgsler venter meget på hinanden | WaitStatsCollector | Medium; High fra 16 ventende tasks i gennemsnit. |
| SQLI-WAIT-007 | [SQLI-WAIT-007.sql](SQLI-WAIT-007.sql) | Contention på sider i hukommelsen (PAGELATCH) | WaitStatsCollector | Medium; High fra 4 ventende tasks i gennemsnit. |
| SQLI-WIN-001 | [SQLI-WIN-001.sql](SQLI-WIN-001.sql) | CPU-forbruget er højt i længere tid | CimWindowsServerSource (Windows, ingen SQL) | High; Critical når gennemsnittet er 98 % eller derover. |
| SQLI-WIN-002 | [SQLI-WIN-002.sql](SQLI-WIN-002.sql) | Serveren har næsten ingen ledig hukommelse | CimWindowsServerSource (Windows, ingen SQL) | Medium; High under 2 % og under 256 MB. |
| SQLI-WIN-003 | [SQLI-WIN-003.sql](SQLI-WIN-003.sql) | Serveren pager hukommelse til disk | CimWindowsServerSource (Windows, ingen SQL) | Medium. |
| SQLI-WIN-004 | [SQLI-WIN-004.sql](SQLI-WIN-004.sql) | Den reserverede hukommelse (commit) er tæt på grænsen | CimWindowsServerSource (Windows, ingen SQL) | Medium; High ved 97 % eller derover. |
| SQLI-WIN-005 | [SQLI-WIN-005.sql](SQLI-WIN-005.sql) | Lidt ledig plads på et drev | CimWindowsServerSource (Windows, ingen SQL) | Medium under FreePercent, High under CriticalFreePercent, Critical under 1 GB eller 1 %. |
| SQLI-WIN-006 | [SQLI-WIN-006.sql](SQLI-WIN-006.sql) | Et drev bliver fuldt om få dage | CimWindowsServerSource (Windows, ingen SQL) | Medium; High inden for 7 dage; Critical inden for 2 dage. |
| SQLI-WIN-007 | [SQLI-WIN-007.sql](SQLI-WIN-007.sql) | En tjeneste, der skal starte automatisk, kører ikke | CimWindowsServerSource (Windows, ingen SQL) | Medium. |
| SQLI-WIN-008 | [SQLI-WIN-008.sql](SQLI-WIN-008.sql) | En overvåget tjeneste kører ikke | CimWindowsServerSource (Windows, ingen SQL) | High når tjenesten ikke kører; Medium når den ikke findes (eller ikke kan ses). |
| SQLI-WIN-009 | [SQLI-WIN-009.sql](SQLI-WIN-009.sql) | Fejl i Windows' hændelseslog | CimWindowsServerSource (Windows, ingen SQL) | High: disk/filsystem, ressourcer, niveau Critical, tjeneste stoppet 3+ gange. Medium: tjeneste stoppet, program gik ned, Dynamics AX. Low: almindelige fejl der gentager sig. (Uventet nedlukning: se SQLI-WIN-010.) |
| SQLI-WIN-010 | [SQLI-WIN-010.sql](SQLI-WIN-010.sql) | Serveren er genstartet | CimWindowsServerSource (Windows, ingen SQL) | High ved uventet genstart; Info ved genstart uden tegn på at den var uventet; Low når hændelsesloggen ikke kan læses. |
| SQLI-WIN-011 | [SQLI-WIN-011.sql](SQLI-WIN-011.sql) | Serveren er ikke genstartet i lang tid | CimWindowsServerSource (Windows, ingen SQL) | Low. |
| SQLI-WIN-012 | [SQLI-WIN-012.sql](SQLI-WIN-012.sql) | En planlagt opgave fejlede sidste gang, den kørte | CimWindowsServerSource (Windows, ingen SQL) | Medium. |
| SQLI-WIN-013 | [SQLI-WIN-013.sql](SQLI-WIN-013.sql) | En planlagt opgave kørte ikke, da den skulle | CimWindowsServerSource (Windows, ingen SQL) | Medium. |
| COM-AX-001 | [COM-AX-001.sql](COM-AX-001.sql) | Dynamics AX 3.0 er uden support fra Microsoft | CustomQueryCollector | Medium |
| COM-AX-002 | [COM-AX-002.sql](COM-AX-002.sql) | Dynamics AX 3.0 kører på en SQL Server-version, der ikke var understøttet til AX 3.0 | CustomQueryCollector | Medium |
| COM-AX-003 | [COM-AX-003.sql](COM-AX-003.sql) | Dynamics AX-database på en instans, hvor MAXDOP ikke er 1 (SQL Server 2005-2014) | CustomQueryCollector | Medium |
@ -273,7 +220,7 @@ testen på de filer, der skal opdateres.
| [ReplicationCollector](_collectors/ReplicationCollector.sql) | `src/SQLInsight.Collectors/Instance/ReplicationCollector.cs` | 15 min som standard (SQLInsight:CollectorIntervals:replication) | SQL Server 2005 og nyere | 2 regler |
| [PerformanceCounterCollector](_collectors/PerformanceCounterCollector.sql) | `src/SQLInsight.Collectors/Performance/PerformanceCounterCollector.cs` | 5 min som standard (SQLInsight:CollectorIntervals:performance-counters) | Alle (SQL Server 2000 og nyere) | 2 regler |
| [CpuCollector](_collectors/CpuCollector.sql) | `src/SQLInsight.Collectors/Performance/CpuCollector.cs` | 5 min som standard (SQLInsight:CollectorIntervals:cpu) | SQL Server 2005 og nyere | 2 regler |
| [BlockingCollector](_collectors/BlockingCollector.sql) | `src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs` | 1 min som standard (SQLInsight:CollectorIntervals:blocking) | Alle (SQL Server 2000 og nyere) | 2 regler |
| [BlockingCollector](_collectors/BlockingCollector.sql) | `src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs` | 1 min som standard (SQLInsight:CollectorIntervals:blocking) | Alle (SQL Server 2000 og nyere) | 1 regel |
| [ActivityCollector](_collectors/ActivityCollector.sql) | `src/SQLInsight.Collectors/Concurrency/ActivityCollector.cs` | 1 min som standard (SQLInsight:CollectorIntervals:activity) | SQL Server 2005 og nyere | 3 regler |
| [DeadlockCollector](_collectors/DeadlockCollector.sql) | `src/SQLInsight.Collectors/Concurrency/DeadlockCollector.cs` | 5 min som standard (SQLInsight:CollectorIntervals:deadlocks) | SQL Server 2008 og nyere (filer fra 2012, ellers ring buffer) | 1 regel |
| [QueryStoreCollector](_collectors/QueryStoreCollector.sql) | `src/SQLInsight.Collectors/Performance/QueryStoreCollector.cs` | 30 min som standard (SQLInsight:CollectorIntervals:query-store) | SQL Server 2016 og nyere | 3 regler |
@ -282,7 +229,3 @@ testen på de filer, der skal opdateres.
| [AvailabilityGroupCollector](_collectors/AvailabilityGroupCollector.sql) | `src/SQLInsight.Collectors/Instance/AvailabilityGroupCollector.cs` | 1 min som standard (SQLInsight:CollectorIntervals:availability-groups) | SQL Server 2012 og nyere med Always On slået til | 3 regler |
| [AgentJobCollector](_collectors/AgentJobCollector.sql) | `src/SQLInsight.Collectors/Instance/AgentJobCollector.cs` | 5 min som standard (SQLInsight:CollectorIntervals:agent-jobs) | SQL Server 2005 og nyere | 2 regler |
| [CustomQueryCollector](_collectors/CustomQueryCollector.sql) | `src/SQLInsight.Collectors/Custom/CustomQueryCollector.cs` | 15 min som standard (SQLInsight:CollectorIntervals:custom-rules) | SQL Server 2005 og nyere (pr. regel: minimumVersion) | 16 regler |
| [AxDatabaseDetector](_collectors/AxDatabaseDetector.sql) | `src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs` | 1 time som standard (SQLInsight:CollectorIntervals:dynamics-ax); uden AX-læseadgang hvert 5. minut | SQL Server 2005 og nyere | 17 regler (alle AX-regler) |
| [AxBatchCollector](_collectors/AxBatchCollector.sql) | `src/SQLInsight.Collectors/DynamicsAx/AxBatchCollector.cs` | 5 min som standard (SQLInsight:CollectorIntervals:dynamics-ax-batch) | SQL Server 2005 og nyere; kræver valgfri AX-læseadgang | 4 regler |
| [AxSequenceCollector](_collectors/AxSequenceCollector.sql) | `src/SQLInsight.Collectors/DynamicsAx/AxSequenceCollector.cs` | 1 time som standard (SQLInsight:CollectorIntervals:dynamics-ax-sequences) | SQL Server 2005 og nyere; kræver valgfri AX-læseadgang | 5 regler |
| [SyntheticCheckRunner](_collectors/SyntheticCheckRunner.sql) | `src/SQLInsight.Servers/Synthetic/SyntheticCheckRunner.cs` | tjekkets eget interval (standard 300 sek, mindst 60) og "Kør nu" | SQL Server 2005 og nyere | 2 regler (SQLI-SYN-001, SQLI-SYN-002) |

View file

@ -1,159 +0,0 @@
-- ===================================================================================================================
-- SQLI-AX-001: Dynamics AX-batchjob er endt med fejl
-- ===================================================================================================================
-- Regel-id: SQLI-AX-001
-- Titel: Dynamics AX-batchjob er endt med fejl
-- Alvor: High
-- Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema (docs/dynamics-ax/
-- schema-ax30.tsv). AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- Et batchjob i Dynamics AX er stoppet med fejl. Det, jobbet skulle gøre (bogføring, lagerafslutning, integration
-- ...), er ikke gjort eller kun delvist, og gentagne job fortsætter ikke nødvendigvis af sig selv.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- Et fund pr. job, når jobbet nu har status Fejl og sluttede (eller var planlagt) inden for grænsen, eller når jobbet
-- venter på næste gentagelse, men dets seneste kørsel i SQLInsights egen historik (insight.AxBatchRuns) endte med
-- fejl inden for grænsen.
-- STATUS oversættes af versionens adapter: AX 2009+ 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet,
-- 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret; Axapta 3.0/4.0 samme rækkefølge (1 og 4 kontrolleret mod kundedata 2026-10-03):
-- 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet. Højst 25 fund pr. database.
--
-- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi):
-- SQLInsight:DynamicsAx:BatchFailedLookbackHours = 24: fejl inden for så mange timer rapporteres.
--
-- Rettigheder til agentens login (se db/permissions/grant-least-privilege.sql og grant-dynamics-ax-read.sql):
-- Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql; [bruger] = agentens databasebruger):
-- Axapta 3.0: GRANT SELECT ON [bmssa].[BATCH] TO [bruger];
-- AX 4.0: GRANT SELECT ON [dbo].[BATCH] TO [bruger];
-- AX 2009/2012/F&O: GRANT SELECT ON [dbo].[BATCHJOB] TO [bruger];
-- GRANT SELECT ON [dbo].[BATCHJOBHISTORY] TO [bruger];
-- Mangler den, er reglen "ikke målbar" med den nøjagtige GRANT for databasen (også under Dynamics AX → AX-databaser
-- og GET /api/v1/dynamics-ax/grant-script). Uden GRANT ændres intet.
-- Genkendelsen: kun grant-least-privilege.sql (VIEW ANY DEFINITION; CONNECT ANY DATABASE fra 2014, ellers en
-- databasebruger: CREATE USER [agent] FOR LOGIN [agent];).
--
-- Data kommer fra (collector, fil : medlemmer, interval):
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- AxBatchCollector
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxBatchCollector.cs; SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs
-- Medlemmer: AxSchemaAdapter.BatchJobs(database), AxSchemaAdapter.BatchHistory(database, afterRecId), AxSql.InDatabase
-- Interval: 5 min som standard (SQLInsight:CollectorIntervals:dynamics-ax-batch)
-- Delt script: SQL Scripts/_collectors/AxBatchCollector.sql
-- Regelkode: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs : AxBatchFailedRule
--
-- Read-only: Ja: kun SELECT med TOP og SET-sessionsindstillinger. AX-tabellerne læses i AX-databasen via
-- [db].sys.sp_executesql (sessionen skifter aldrig database) under READ COMMITTED med SET LOCK_TIMEOUT 2000
-- (SNAPSHOT, når databasen tillader det), aldrig NOLOCK; bagefter sættes agentens sessionsindstillinger
-- igen. Intet ændrer data, indstillinger eller objekter. Kun i databaser genkendt som AX og valgt til
-- overvågning.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); varianter for andre AX-versioner end din fejler eller giver ingen rækker - det er forventet.
-- AX-forespørgslerne er vist for databasen AXDB: ret [AXDB] til din AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
-- Forbindelsen: Sessionsindstillinger, agenten sætter på hver forbindelse
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner: sættes på hver forbindelse, agenten åbner
-- Kilde: SqlConnectionFactory.SessionSettings
-- Kode: src/SQLInsight.Connectivity/SqlConnectionFactory.cs : SqlConnectionFactory.SessionSettings
SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke overvågede databaser er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxBatchCollector: Batchjob og kørselshistorik i AX-databasen (valgfri AX-læseadgang)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Axapta 3.0 (skema bmssa, verificeret mod kundedata): batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(3.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[BATCH] b ORDER BY b.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 4.0 (skema dbo) - ikke verificeret mod kundedata: batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(4.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCH] b ORDER BY b.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(2009)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (de nyeste)
-- Kilde: AxSchemaAdapter.BatchHistory(2009, første)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId: null) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger (fra den højeste RECID, SQLInsight har læst, minus overlappet)
-- Kilde: AxSchemaAdapter.BatchHistory(2009, næste)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId) via AxSql.InDatabase
-- Agenten indsætter den højeste RECID, den har læst, minus overlappet, hvor der her står 0.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (3000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 0 ORDER BY h.[RECID]';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(2012)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (de nyeste)
-- Kilde: AxSchemaAdapter.BatchHistory(2012, første)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId: null) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger (fra den højeste RECID, SQLInsight har læst, minus overlappet)
-- Kilde: AxSchemaAdapter.BatchHistory(2012, næste)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId) via AxSql.InDatabase
-- Agenten indsætter den højeste RECID, den har læst, minus overlappet, hvor der her står 0.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (3000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 0 ORDER BY h.[RECID]';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO

View file

@ -1,163 +0,0 @@
-- ===================================================================================================================
-- SQLI-AX-002: Dynamics AX-batchjob kører længere end normalt
-- ===================================================================================================================
-- Regel-id: SQLI-AX-002
-- Titel: Dynamics AX-batchjob kører længere end normalt
-- Alvor: Medium; High, når kørslen er dobbelt så lang som grænsen.
-- Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema (docs/dynamics-ax/
-- schema-ax30.tsv). AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- Et kørende batchjob har kørt meget længere end jobbets egne seneste kørsler. Det kan skyldes blokering, en ændret
-- datamængde, en dårlig plan, eller at jobbet hænger - og imens kan det holde låse, som brugerne venter på.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- For job med status Udfører: kørt = nu - faktisk start (AX 2009+: BATCHJOB.STARTDATETIME; Axapta 3.0/4.0 gemmer
-- ikke den faktiske start, så det er første gang, SQLInsight så jobbet køre). Grænse = max(faktor x 90 %-fraktil af
-- jobbets seneste op til 20 vellykkede kørsler, minimum). Historikken er SQLInsights egen (insight.AxBatchRuns): fra
-- BATCHJOBHISTORY i AX 2009+, ellers de kørsler, SQLInsight selv ser blive afsluttet. Hænger jobbet (SQLI-AX-004),
-- gives kun det fund.
-- STATUS oversættes af versionens adapter: AX 2009+ 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet,
-- 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret; Axapta 3.0/4.0 samme rækkefølge (1 og 4 kontrolleret mod kundedata 2026-10-03):
-- 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet. Højst 25 fund pr. database.
--
-- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi):
-- SQLInsight:DynamicsAx:BatchLongRunningFactor = 2.0: kørt mere end så mange gange jobbets 90 %-fraktil
-- SQLInsight:DynamicsAx:BatchLongRunningMinimumMinutes = 10: men aldrig under så mange minutter
-- SQLInsight:DynamicsAx:BatchHistoryMinimumRuns = 5: så mange tidligere kørsler skal der være
--
-- Rettigheder til agentens login (se db/permissions/grant-least-privilege.sql og grant-dynamics-ax-read.sql):
-- Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql; [bruger] = agentens databasebruger):
-- Axapta 3.0: GRANT SELECT ON [bmssa].[BATCH] TO [bruger];
-- AX 4.0: GRANT SELECT ON [dbo].[BATCH] TO [bruger];
-- AX 2009/2012/F&O: GRANT SELECT ON [dbo].[BATCHJOB] TO [bruger];
-- GRANT SELECT ON [dbo].[BATCHJOBHISTORY] TO [bruger];
-- Mangler den, er reglen "ikke målbar" med den nøjagtige GRANT for databasen (også under Dynamics AX → AX-databaser
-- og GET /api/v1/dynamics-ax/grant-script). Uden GRANT ændres intet.
-- Genkendelsen: kun grant-least-privilege.sql (VIEW ANY DEFINITION; CONNECT ANY DATABASE fra 2014, ellers en
-- databasebruger: CREATE USER [agent] FOR LOGIN [agent];).
--
-- Data kommer fra (collector, fil : medlemmer, interval):
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- AxBatchCollector
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxBatchCollector.cs; SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs
-- Medlemmer: AxSchemaAdapter.BatchJobs(database), AxSchemaAdapter.BatchHistory(database, afterRecId), AxSql.InDatabase
-- Interval: 5 min som standard (SQLInsight:CollectorIntervals:dynamics-ax-batch)
-- Delt script: SQL Scripts/_collectors/AxBatchCollector.sql
-- Regelkode: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs : AxBatchLongRunningRule
--
-- Read-only: Ja: kun SELECT med TOP og SET-sessionsindstillinger. AX-tabellerne læses i AX-databasen via
-- [db].sys.sp_executesql (sessionen skifter aldrig database) under READ COMMITTED med SET LOCK_TIMEOUT 2000
-- (SNAPSHOT, når databasen tillader det), aldrig NOLOCK; bagefter sættes agentens sessionsindstillinger
-- igen. Intet ændrer data, indstillinger eller objekter. Kun i databaser genkendt som AX og valgt til
-- overvågning.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); varianter for andre AX-versioner end din fejler eller giver ingen rækker - det er forventet.
-- AX-forespørgslerne er vist for databasen AXDB: ret [AXDB] til din AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
-- Forbindelsen: Sessionsindstillinger, agenten sætter på hver forbindelse
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner: sættes på hver forbindelse, agenten åbner
-- Kilde: SqlConnectionFactory.SessionSettings
-- Kode: src/SQLInsight.Connectivity/SqlConnectionFactory.cs : SqlConnectionFactory.SessionSettings
SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke overvågede databaser er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxBatchCollector: Batchjob og kørselshistorik i AX-databasen (valgfri AX-læseadgang)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Axapta 3.0 (skema bmssa, verificeret mod kundedata): batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(3.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[BATCH] b ORDER BY b.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 4.0 (skema dbo) - ikke verificeret mod kundedata: batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(4.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCH] b ORDER BY b.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(2009)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (de nyeste)
-- Kilde: AxSchemaAdapter.BatchHistory(2009, første)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId: null) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger (fra den højeste RECID, SQLInsight har læst, minus overlappet)
-- Kilde: AxSchemaAdapter.BatchHistory(2009, næste)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId) via AxSql.InDatabase
-- Agenten indsætter den højeste RECID, den har læst, minus overlappet, hvor der her står 0.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (3000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 0 ORDER BY h.[RECID]';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(2012)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (de nyeste)
-- Kilde: AxSchemaAdapter.BatchHistory(2012, første)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId: null) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger (fra den højeste RECID, SQLInsight har læst, minus overlappet)
-- Kilde: AxSchemaAdapter.BatchHistory(2012, næste)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId) via AxSql.InDatabase
-- Agenten indsætter den højeste RECID, den har læst, minus overlappet, hvor der her står 0.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (3000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 0 ORDER BY h.[RECID]';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO

View file

@ -1,164 +0,0 @@
-- ===================================================================================================================
-- SQLI-AX-003: Dynamics AX-batchjob er ikke startet til tiden
-- ===================================================================================================================
-- Regel-id: SQLI-AX-003
-- Titel: Dynamics AX-batchjob er ikke startet til tiden
-- Alvor: Medium; High, når jobbet i praksis aldrig er startet.
-- Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema (docs/dynamics-ax/
-- schema-ax30.tsv). AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- Et ventende batchjob, hvis planlagte start er passeret: batchserveren (AOS med batch, eller batch-klienten i
-- Axapta 3.0) tager det ikke. Mange forsinkede job på én gang betyder typisk, at batchserveren slet ikke kører.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- For job med status Venter: forsinkelse = nu - planlagt start. Planlagt start er i AX 2009+ BATCHJOB.ORIGSTARTDATETIME
-- (UTC). I Axapta 3.0/4.0 er den BATCH.STARTDATE (lokal dato) + STARTTIME (sekunder efter midnat, lokal tid); den
-- regnes om til UTC med AX-tidszonen (SQLInsight:DynamicsAx:LocalTimeZone, ellers agentens tidszone) og de
-- sommertidsregler, der gjaldt på datoen. AX' tomme dato (1900-01-01) er "ingen". Job med status Tilbageholdt er
-- ikke forsinkede. De mest forsinkede først.
-- STATUS oversættes af versionens adapter: AX 2009+ 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet,
-- 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret; Axapta 3.0/4.0 samme rækkefølge: 0 Tilbageholdt, 1 Venter,
-- 2 Udfører, 3 Fejl, 4 Afsluttet (1 og 4 kontrolleret mod kundedata 2026-10-03). Højst 25 fund pr. database.
-- Job planlagt for mere end BatchAbandonedDays siden regnes som opgivne rækker og meldes ikke.
--
-- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi):
-- SQLInsight:DynamicsAx:BatchLateMinutes = 30: forsinket mere end så mange minutter: Medium
-- SQLInsight:DynamicsAx:BatchNeverStartedHours = 24: forsinket mere end så mange timer: High, "aldrig startet"
-- SQLInsight:DynamicsAx:BatchAbandonedDays = 30: planlagt for mere end så mange dage siden: opgivet, ingen fund
--
-- Rettigheder til agentens login (se db/permissions/grant-least-privilege.sql og grant-dynamics-ax-read.sql):
-- Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql; [bruger] = agentens databasebruger):
-- Axapta 3.0: GRANT SELECT ON [bmssa].[BATCH] TO [bruger];
-- AX 4.0: GRANT SELECT ON [dbo].[BATCH] TO [bruger];
-- AX 2009/2012/F&O: GRANT SELECT ON [dbo].[BATCHJOB] TO [bruger];
-- GRANT SELECT ON [dbo].[BATCHJOBHISTORY] TO [bruger];
-- Mangler den, er reglen "ikke målbar" med den nøjagtige GRANT for databasen (også under Dynamics AX → AX-databaser
-- og GET /api/v1/dynamics-ax/grant-script). Uden GRANT ændres intet.
-- Genkendelsen: kun grant-least-privilege.sql (VIEW ANY DEFINITION; CONNECT ANY DATABASE fra 2014, ellers en
-- databasebruger: CREATE USER [agent] FOR LOGIN [agent];).
--
-- Data kommer fra (collector, fil : medlemmer, interval):
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- AxBatchCollector
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxBatchCollector.cs; SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs
-- Medlemmer: AxSchemaAdapter.BatchJobs(database), AxSchemaAdapter.BatchHistory(database, afterRecId), AxSql.InDatabase
-- Interval: 5 min som standard (SQLInsight:CollectorIntervals:dynamics-ax-batch)
-- Delt script: SQL Scripts/_collectors/AxBatchCollector.sql
-- Regelkode: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs : AxBatchLateRule
--
-- Read-only: Ja: kun SELECT med TOP og SET-sessionsindstillinger. AX-tabellerne læses i AX-databasen via
-- [db].sys.sp_executesql (sessionen skifter aldrig database) under READ COMMITTED med SET LOCK_TIMEOUT 2000
-- (SNAPSHOT, når databasen tillader det), aldrig NOLOCK; bagefter sættes agentens sessionsindstillinger
-- igen. Intet ændrer data, indstillinger eller objekter. Kun i databaser genkendt som AX og valgt til
-- overvågning.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); varianter for andre AX-versioner end din fejler eller giver ingen rækker - det er forventet.
-- AX-forespørgslerne er vist for databasen AXDB: ret [AXDB] til din AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
-- Forbindelsen: Sessionsindstillinger, agenten sætter på hver forbindelse
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner: sættes på hver forbindelse, agenten åbner
-- Kilde: SqlConnectionFactory.SessionSettings
-- Kode: src/SQLInsight.Connectivity/SqlConnectionFactory.cs : SqlConnectionFactory.SessionSettings
SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke overvågede databaser er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxBatchCollector: Batchjob og kørselshistorik i AX-databasen (valgfri AX-læseadgang)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Axapta 3.0 (skema bmssa, verificeret mod kundedata): batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(3.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[BATCH] b ORDER BY b.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 4.0 (skema dbo) - ikke verificeret mod kundedata: batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(4.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCH] b ORDER BY b.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(2009)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (de nyeste)
-- Kilde: AxSchemaAdapter.BatchHistory(2009, første)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId: null) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger (fra den højeste RECID, SQLInsight har læst, minus overlappet)
-- Kilde: AxSchemaAdapter.BatchHistory(2009, næste)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId) via AxSql.InDatabase
-- Agenten indsætter den højeste RECID, den har læst, minus overlappet, hvor der her står 0.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (3000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 0 ORDER BY h.[RECID]';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(2012)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (de nyeste)
-- Kilde: AxSchemaAdapter.BatchHistory(2012, første)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId: null) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger (fra den højeste RECID, SQLInsight har læst, minus overlappet)
-- Kilde: AxSchemaAdapter.BatchHistory(2012, næste)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId) via AxSql.InDatabase
-- Agenten indsætter den højeste RECID, den har læst, minus overlappet, hvor der her står 0.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (3000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 0 ORDER BY h.[RECID]';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO

View file

@ -1,163 +0,0 @@
-- ===================================================================================================================
-- SQLI-AX-004: Dynamics AX-batchjob hænger i Udfører
-- ===================================================================================================================
-- Regel-id: SQLI-AX-004
-- Titel: Dynamics AX-batchjob hænger i Udfører
-- Alvor: High
-- Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema (docs/dynamics-ax/
-- schema-ax30.tsv). AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- Et batchjob har haft status Udfører i flere timer. Typisk er AOS'en eller batch-klienten genstartet eller gået ned
-- midt i kørslen, så jobbet aldrig blev afsluttet: det står som kørende, intet sker, og gentagne job planlægges ikke
-- igen.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- For job med status Udfører: kørt = nu - faktisk start (AX 2009+: BATCHJOB.STARTDATETIME; Axapta 3.0/4.0: første
-- gang, SQLInsight så jobbet køre, så det kan have kørt længere). Fund, når kørt >= BatchStuckHours, medmindre jobbet
-- efter sin egen historik (insight.AxBatchRuns, mindst BatchHistoryMinimumRuns kørsler) normalt kører så længe
-- (kørt <= faktor x 90 %-fraktil).
-- STATUS oversættes af versionens adapter: AX 2009+ 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet,
-- 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret; Axapta 3.0/4.0 samme rækkefølge (1 og 4 kontrolleret mod kundedata 2026-10-03):
-- 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet. Højst 25 fund pr. database.
--
-- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi):
-- SQLInsight:DynamicsAx:BatchStuckHours = 6: kørt mindst så mange timer
-- SQLInsight:DynamicsAx:BatchLongRunningFactor = 2.0: ...og mere end så mange gange jobbets 90 %-fraktil, når der er
-- historik
--
-- Rettigheder til agentens login (se db/permissions/grant-least-privilege.sql og grant-dynamics-ax-read.sql):
-- Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql; [bruger] = agentens databasebruger):
-- Axapta 3.0: GRANT SELECT ON [bmssa].[BATCH] TO [bruger];
-- AX 4.0: GRANT SELECT ON [dbo].[BATCH] TO [bruger];
-- AX 2009/2012/F&O: GRANT SELECT ON [dbo].[BATCHJOB] TO [bruger];
-- GRANT SELECT ON [dbo].[BATCHJOBHISTORY] TO [bruger];
-- Mangler den, er reglen "ikke målbar" med den nøjagtige GRANT for databasen (også under Dynamics AX → AX-databaser
-- og GET /api/v1/dynamics-ax/grant-script). Uden GRANT ændres intet.
-- Genkendelsen: kun grant-least-privilege.sql (VIEW ANY DEFINITION; CONNECT ANY DATABASE fra 2014, ellers en
-- databasebruger: CREATE USER [agent] FOR LOGIN [agent];).
--
-- Data kommer fra (collector, fil : medlemmer, interval):
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- AxBatchCollector
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxBatchCollector.cs; SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs
-- Medlemmer: AxSchemaAdapter.BatchJobs(database), AxSchemaAdapter.BatchHistory(database, afterRecId), AxSql.InDatabase
-- Interval: 5 min som standard (SQLInsight:CollectorIntervals:dynamics-ax-batch)
-- Delt script: SQL Scripts/_collectors/AxBatchCollector.sql
-- Regelkode: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs : AxBatchStuckRule
--
-- Read-only: Ja: kun SELECT med TOP og SET-sessionsindstillinger. AX-tabellerne læses i AX-databasen via
-- [db].sys.sp_executesql (sessionen skifter aldrig database) under READ COMMITTED med SET LOCK_TIMEOUT 2000
-- (SNAPSHOT, når databasen tillader det), aldrig NOLOCK; bagefter sættes agentens sessionsindstillinger
-- igen. Intet ændrer data, indstillinger eller objekter. Kun i databaser genkendt som AX og valgt til
-- overvågning.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); varianter for andre AX-versioner end din fejler eller giver ingen rækker - det er forventet.
-- AX-forespørgslerne er vist for databasen AXDB: ret [AXDB] til din AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
-- Forbindelsen: Sessionsindstillinger, agenten sætter på hver forbindelse
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner: sættes på hver forbindelse, agenten åbner
-- Kilde: SqlConnectionFactory.SessionSettings
-- Kode: src/SQLInsight.Connectivity/SqlConnectionFactory.cs : SqlConnectionFactory.SessionSettings
SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke overvågede databaser er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxBatchCollector: Batchjob og kørselshistorik i AX-databasen (valgfri AX-læseadgang)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Axapta 3.0 (skema bmssa, verificeret mod kundedata): batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(3.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[BATCH] b ORDER BY b.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 4.0 (skema dbo) - ikke verificeret mod kundedata: batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(4.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCH] b ORDER BY b.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(2009)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (de nyeste)
-- Kilde: AxSchemaAdapter.BatchHistory(2009, første)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId: null) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger (fra den højeste RECID, SQLInsight har læst, minus overlappet)
-- Kilde: AxSchemaAdapter.BatchHistory(2009, næste)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId) via AxSql.InDatabase
-- Agenten indsætter den højeste RECID, den har læst, minus overlappet, hvor der her står 0.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (3000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 0 ORDER BY h.[RECID]';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(2012)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (de nyeste)
-- Kilde: AxSchemaAdapter.BatchHistory(2012, første)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId: null) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger (fra den højeste RECID, SQLInsight har læst, minus overlappet)
-- Kilde: AxSchemaAdapter.BatchHistory(2012, næste)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId) via AxSql.InDatabase
-- Agenten indsætter den højeste RECID, den har læst, minus overlappet, hvor der her står 0.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (3000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 0 ORDER BY h.[RECID]';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO

View file

@ -1,131 +0,0 @@
-- ===================================================================================================================
-- SQLI-AX-005: Dynamics AX-nummerserie er ved at løbe tør
-- ===================================================================================================================
-- Regel-id: SQLI-AX-005
-- Titel: Dynamics AX-nummerserie er ved at løbe tør
-- Alvor: Medium; High over den kritiske procent eller inden for de kritiske dage; Critical, når serien er opbrugt.
-- Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema (docs/dynamics-ax/
-- schema-ax30.tsv). AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- Når en nummerserie er brugt op, kan AX ikke tildele flere numre: oprettelse eller bogføring af fx ordrer, fakturaer
-- eller posteringer, der bruger serien, fejler for alle brugere.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- Grænse = HIGHEST, eller formatets kapacitet (10^antal # - 1), hvis den er lavere. Brugt % = (NEXTREC - LOWEST) /
-- (grænse - LOWEST + 1). Tilbage = grænse - NEXTREC + 1. Prognose: forbrug pr. dag = (NEXTREC nu - NEXTREC i den
-- ældste daglige måling i vinduet) / dage imellem; dage tilbage = (grænse - NEXTREC) / forbrug pr. dag. Målingerne er
-- SQLInsights egne (insight.AxSequenceSamples, én pr. serie pr. dag). Manuelle, cykliske og spærrede serier springes
-- over. Højst 25 fund pr. database. Tal: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : Interpret, AxForecast.From.
--
-- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi):
-- SQLInsight:DynamicsAx:NumberSequenceWarnPercent = 80: brugt mindst så mange procent: Medium
-- SQLInsight:DynamicsAx:NumberSequenceCriticalPercent = 95: High
-- SQLInsight:DynamicsAx:ForecastWarnDays = 90: løber tør inden for så mange dage efter prognosen: Medium
-- SQLInsight:DynamicsAx:ForecastCriticalDays = 30: High
-- SQLInsight:DynamicsAx:ForecastBasisDays = 30: prognosen bruger den ældste daglige måling inden for så mange dage
--
-- Rettigheder til agentens login (se db/permissions/grant-least-privilege.sql og grant-dynamics-ax-read.sql):
-- Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql; [bruger] = agentens databasebruger):
-- Axapta 3.0: GRANT SELECT ON [bmssa].[NUMBERSEQUENCETABLE] TO [bruger];
-- GRANT SELECT ON [bmssa].[NUMBERSEQUENCEREFERENCE] TO [bruger]; -- valgfri: om serien bruges
-- AX 4.0/2009: de samme tabeller i [dbo]
-- AX 2012/F&O: desuden GRANT SELECT ON [dbo].[NUMBERSEQUENCESCOPE] TO [bruger]; -- valgfri: firma
-- Mangler NUMBERSEQUENCETABLE, er reglen "ikke målbar" med den nøjagtige GRANT for databasen. Uden GRANT ændres intet.
-- Genkendelsen: kun grant-least-privilege.sql (VIEW ANY DEFINITION; CONNECT ANY DATABASE fra 2014, ellers en
-- databasebruger: CREATE USER [agent] FOR LOGIN [agent];).
--
-- Data kommer fra (collector, fil : medlemmer, interval):
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- AxSequenceCollector
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxSequenceCollector.cs; SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs
-- Medlemmer: AxSchemaAdapter.NumberSequences(database), AxSchemaAdapter.RecIds(database), AxSql.InDatabase
-- Interval: 1 time som standard (SQLInsight:CollectorIntervals:dynamics-ax-sequences)
-- Delt script: SQL Scripts/_collectors/AxSequenceCollector.sql
-- Regelkode: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs : AxNumberSequenceExhaustionRule
--
-- Read-only: Ja: kun SELECT med TOP og SET-sessionsindstillinger. AX-tabellerne læses i AX-databasen via
-- [db].sys.sp_executesql (sessionen skifter aldrig database) under READ COMMITTED med SET LOCK_TIMEOUT 2000
-- (SNAPSHOT, når databasen tillader det), aldrig NOLOCK; bagefter sættes agentens sessionsindstillinger
-- igen. Intet ændrer data, indstillinger eller objekter. Kun i databaser genkendt som AX og valgt til
-- overvågning.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); varianter for andre AX-versioner end din fejler eller giver ingen rækker - det er forventet.
-- AX-forespørgslerne er vist for databasen AXDB: ret [AXDB] til din AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
-- Forbindelsen: Sessionsindstillinger, agenten sætter på hver forbindelse
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner: sættes på hver forbindelse, agenten åbner
-- Kilde: SqlConnectionFactory.SessionSettings
-- Kode: src/SQLInsight.Connectivity/SqlConnectionFactory.cs : SqlConnectionFactory.SessionSettings
SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke overvågede databaser er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxSequenceCollector: Nummerserier i AX-databasen (valgfri AX-læseadgang)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Axapta 3.0 (skema bmssa, verificeret mod kundedata): nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(3.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [bmssa].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 4.0 (skema dbo) - ikke verificeret mod kundedata: nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(4.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(2009)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(2012)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], NULLIF(sc.[DATAAREA], N''''), n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCEID] = n.[RECID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n LEFT JOIN [dbo].[NUMBERSEQUENCESCOPE] sc ON sc.[RECID] = n.[NUMBERSEQUENCESCOPE] ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO

View file

@ -1,124 +0,0 @@
-- ===================================================================================================================
-- SQLI-AX-006: Dynamics AX-nummerseriens format er for kort til Højeste
-- ===================================================================================================================
-- Regel-id: SQLI-AX-006
-- Titel: Dynamics AX-nummerseriens format er for kort til Højeste
-- Alvor: Medium; High, når NEXTREC allerede er større end formatets kapacitet.
-- Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema (docs/dynamics-ax/
-- schema-ax30.tsv). AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- Formatet (fx "FA-####") har færre cifre, end HIGHEST kræver. Når serien når forbi formatets kapacitet, fejler
-- tildelingen af numre, selv om serien ikke er brugt op.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- Kapacitet = 10^(antal # i FORMAT) - 1. Fund, når HIGHEST > kapacitet (ikke for manuelle eller spærrede serier). High,
-- når også NEXTREC > kapacitet. Højst 25 fund pr. database.
--
-- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi):
-- Ingen konfigurationsnøgle: ren sammenligning.
--
-- Rettigheder til agentens login (se db/permissions/grant-least-privilege.sql og grant-dynamics-ax-read.sql):
-- Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql; [bruger] = agentens databasebruger):
-- Axapta 3.0: GRANT SELECT ON [bmssa].[NUMBERSEQUENCETABLE] TO [bruger];
-- GRANT SELECT ON [bmssa].[NUMBERSEQUENCEREFERENCE] TO [bruger]; -- valgfri: om serien bruges
-- AX 4.0/2009: de samme tabeller i [dbo]
-- AX 2012/F&O: desuden GRANT SELECT ON [dbo].[NUMBERSEQUENCESCOPE] TO [bruger]; -- valgfri: firma
-- Mangler NUMBERSEQUENCETABLE, er reglen "ikke målbar" med den nøjagtige GRANT for databasen. Uden GRANT ændres intet.
-- Genkendelsen: kun grant-least-privilege.sql (VIEW ANY DEFINITION; CONNECT ANY DATABASE fra 2014, ellers en
-- databasebruger: CREATE USER [agent] FOR LOGIN [agent];).
--
-- Data kommer fra (collector, fil : medlemmer, interval):
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- AxSequenceCollector
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxSequenceCollector.cs; SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs
-- Medlemmer: AxSchemaAdapter.NumberSequences(database), AxSchemaAdapter.RecIds(database), AxSql.InDatabase
-- Interval: 1 time som standard (SQLInsight:CollectorIntervals:dynamics-ax-sequences)
-- Delt script: SQL Scripts/_collectors/AxSequenceCollector.sql
-- Regelkode: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs : AxNumberSequenceFormatRule
--
-- Read-only: Ja: kun SELECT med TOP og SET-sessionsindstillinger. AX-tabellerne læses i AX-databasen via
-- [db].sys.sp_executesql (sessionen skifter aldrig database) under READ COMMITTED med SET LOCK_TIMEOUT 2000
-- (SNAPSHOT, når databasen tillader det), aldrig NOLOCK; bagefter sættes agentens sessionsindstillinger
-- igen. Intet ændrer data, indstillinger eller objekter. Kun i databaser genkendt som AX og valgt til
-- overvågning.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); varianter for andre AX-versioner end din fejler eller giver ingen rækker - det er forventet.
-- AX-forespørgslerne er vist for databasen AXDB: ret [AXDB] til din AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
-- Forbindelsen: Sessionsindstillinger, agenten sætter på hver forbindelse
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner: sættes på hver forbindelse, agenten åbner
-- Kilde: SqlConnectionFactory.SessionSettings
-- Kode: src/SQLInsight.Connectivity/SqlConnectionFactory.cs : SqlConnectionFactory.SessionSettings
SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke overvågede databaser er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxSequenceCollector: Nummerserier i AX-databasen (valgfri AX-læseadgang)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Axapta 3.0 (skema bmssa, verificeret mod kundedata): nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(3.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [bmssa].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 4.0 (skema dbo) - ikke verificeret mod kundedata: nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(4.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(2009)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(2012)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], NULLIF(sc.[DATAAREA], N''''), n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCEID] = n.[RECID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n LEFT JOIN [dbo].[NUMBERSEQUENCESCOPE] sc ON sc.[RECID] = n.[NUMBERSEQUENCESCOPE] ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO

View file

@ -1,125 +0,0 @@
-- ===================================================================================================================
-- SQLI-AX-007: Spærret Dynamics AX-nummerserie bruges stadig
-- ===================================================================================================================
-- Regel-id: SQLI-AX-007
-- Titel: Spærret Dynamics AX-nummerserie bruges stadig
-- Alvor: Medium
-- Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema (docs/dynamics-ax/
-- schema-ax30.tsv). AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- Serien er spærret (BLOCKED = 1), men en nummerseriereference peger stadig på den. Alt i AX, der skal have et nummer
-- fra den reference, fejler med en fejl om, at nummerserien er spærret.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- Fund, når BLOCKED = 1 og der findes en NUMBERSEQUENCEREFERENCE til serien (Axapta 3.0/4.0/2009: samme NUMBERSEQUENCE
-- og DATAAREAID; AX 2012/F&O: NUMBERSEQUENCEID = seriens RECID). Kan referencetabellen ikke læses, vides det ikke, og
-- der gives intet fund. Højst 25 fund pr. database.
--
-- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi):
-- Ingen konfigurationsnøgle.
--
-- Rettigheder til agentens login (se db/permissions/grant-least-privilege.sql og grant-dynamics-ax-read.sql):
-- Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql; [bruger] = agentens databasebruger):
-- Axapta 3.0: GRANT SELECT ON [bmssa].[NUMBERSEQUENCETABLE] TO [bruger];
-- GRANT SELECT ON [bmssa].[NUMBERSEQUENCEREFERENCE] TO [bruger]; -- valgfri: om serien bruges
-- AX 4.0/2009: de samme tabeller i [dbo]
-- AX 2012/F&O: desuden GRANT SELECT ON [dbo].[NUMBERSEQUENCESCOPE] TO [bruger]; -- valgfri: firma
-- Mangler NUMBERSEQUENCETABLE, er reglen "ikke målbar" med den nøjagtige GRANT for databasen. Uden GRANT ændres intet.
-- Genkendelsen: kun grant-least-privilege.sql (VIEW ANY DEFINITION; CONNECT ANY DATABASE fra 2014, ellers en
-- databasebruger: CREATE USER [agent] FOR LOGIN [agent];).
--
-- Data kommer fra (collector, fil : medlemmer, interval):
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- AxSequenceCollector
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxSequenceCollector.cs; SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs
-- Medlemmer: AxSchemaAdapter.NumberSequences(database), AxSchemaAdapter.RecIds(database), AxSql.InDatabase
-- Interval: 1 time som standard (SQLInsight:CollectorIntervals:dynamics-ax-sequences)
-- Delt script: SQL Scripts/_collectors/AxSequenceCollector.sql
-- Regelkode: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs : AxNumberSequenceBlockedRule
--
-- Read-only: Ja: kun SELECT med TOP og SET-sessionsindstillinger. AX-tabellerne læses i AX-databasen via
-- [db].sys.sp_executesql (sessionen skifter aldrig database) under READ COMMITTED med SET LOCK_TIMEOUT 2000
-- (SNAPSHOT, når databasen tillader det), aldrig NOLOCK; bagefter sættes agentens sessionsindstillinger
-- igen. Intet ændrer data, indstillinger eller objekter. Kun i databaser genkendt som AX og valgt til
-- overvågning.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); varianter for andre AX-versioner end din fejler eller giver ingen rækker - det er forventet.
-- AX-forespørgslerne er vist for databasen AXDB: ret [AXDB] til din AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
-- Forbindelsen: Sessionsindstillinger, agenten sætter på hver forbindelse
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner: sættes på hver forbindelse, agenten åbner
-- Kilde: SqlConnectionFactory.SessionSettings
-- Kode: src/SQLInsight.Connectivity/SqlConnectionFactory.cs : SqlConnectionFactory.SessionSettings
SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke overvågede databaser er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxSequenceCollector: Nummerserier i AX-databasen (valgfri AX-læseadgang)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Axapta 3.0 (skema bmssa, verificeret mod kundedata): nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(3.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [bmssa].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 4.0 (skema dbo) - ikke verificeret mod kundedata: nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(4.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(2009)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(2012)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], NULLIF(sc.[DATAAREA], N''''), n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCEID] = n.[RECID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n LEFT JOIN [dbo].[NUMBERSEQUENCESCOPE] sc ON sc.[RECID] = n.[NUMBERSEQUENCESCOPE] ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO

View file

@ -1,127 +0,0 @@
-- ===================================================================================================================
-- SQLI-AX-008: Kontinuerlig Dynamics AX-nummerserie er ikke ryddet op
-- ===================================================================================================================
-- Regel-id: SQLI-AX-008
-- Titel: Kontinuerlig Dynamics AX-nummerserie er ikke ryddet op
-- Alvor: Low
-- Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema (docs/dynamics-ax/
-- schema-ax30.tsv). AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- En kontinuerlig serie må ikke have huller. Numre reserveret af afbrudte sessioner bliver liggende som optagede, til
-- serien ryddes op; uden oprydning opstår huller og fejl, og serien bliver en flaskehals, fordi hver tildeling låser
-- rækken i NUMBERSEQUENCETABLE, til transaktionen er færdig.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- For kontinuerlige (CONTINUOUS = 1), ikke spærrede, ikke manuelle serier, der er taget i brug (NEXTREC > LOWEST):
-- fund, når seneste oprydning er ældre end grænsen eller aldrig er sket (1900-01-01). Seneste oprydning er
-- LATESTCLEANDATETIME (UTC) i AX 2009+, og LATESTCLEANDATE + LATESTCLEANTIME (lokal tid, sekunder efter midnat) i
-- Axapta 3.0. Mangler kolonnen, gives intet fund. Højst 25 fund pr. database.
--
-- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi):
-- 7 dage (fast, AxNumberSequenceContinuousRule.MinimumDays), eller 2 x seriens eget CLEANINTERVAL (timer), hvis længere.
--
-- Rettigheder til agentens login (se db/permissions/grant-least-privilege.sql og grant-dynamics-ax-read.sql):
-- Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql; [bruger] = agentens databasebruger):
-- Axapta 3.0: GRANT SELECT ON [bmssa].[NUMBERSEQUENCETABLE] TO [bruger];
-- GRANT SELECT ON [bmssa].[NUMBERSEQUENCEREFERENCE] TO [bruger]; -- valgfri: om serien bruges
-- AX 4.0/2009: de samme tabeller i [dbo]
-- AX 2012/F&O: desuden GRANT SELECT ON [dbo].[NUMBERSEQUENCESCOPE] TO [bruger]; -- valgfri: firma
-- Mangler NUMBERSEQUENCETABLE, er reglen "ikke målbar" med den nøjagtige GRANT for databasen. Uden GRANT ændres intet.
-- Genkendelsen: kun grant-least-privilege.sql (VIEW ANY DEFINITION; CONNECT ANY DATABASE fra 2014, ellers en
-- databasebruger: CREATE USER [agent] FOR LOGIN [agent];).
--
-- Data kommer fra (collector, fil : medlemmer, interval):
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- AxSequenceCollector
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxSequenceCollector.cs; SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs
-- Medlemmer: AxSchemaAdapter.NumberSequences(database), AxSchemaAdapter.RecIds(database), AxSql.InDatabase
-- Interval: 1 time som standard (SQLInsight:CollectorIntervals:dynamics-ax-sequences)
-- Delt script: SQL Scripts/_collectors/AxSequenceCollector.sql
-- Regelkode: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs : AxNumberSequenceContinuousRule
--
-- Read-only: Ja: kun SELECT med TOP og SET-sessionsindstillinger. AX-tabellerne læses i AX-databasen via
-- [db].sys.sp_executesql (sessionen skifter aldrig database) under READ COMMITTED med SET LOCK_TIMEOUT 2000
-- (SNAPSHOT, når databasen tillader det), aldrig NOLOCK; bagefter sættes agentens sessionsindstillinger
-- igen. Intet ændrer data, indstillinger eller objekter. Kun i databaser genkendt som AX og valgt til
-- overvågning.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); varianter for andre AX-versioner end din fejler eller giver ingen rækker - det er forventet.
-- AX-forespørgslerne er vist for databasen AXDB: ret [AXDB] til din AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
-- Forbindelsen: Sessionsindstillinger, agenten sætter på hver forbindelse
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner: sættes på hver forbindelse, agenten åbner
-- Kilde: SqlConnectionFactory.SessionSettings
-- Kode: src/SQLInsight.Connectivity/SqlConnectionFactory.cs : SqlConnectionFactory.SessionSettings
SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke overvågede databaser er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxSequenceCollector: Nummerserier i AX-databasen (valgfri AX-læseadgang)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Axapta 3.0 (skema bmssa, verificeret mod kundedata): nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(3.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [bmssa].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 4.0 (skema dbo) - ikke verificeret mod kundedata: nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(4.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(2009)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(2012)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], NULLIF(sc.[DATAAREA], N''''), n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCEID] = n.[RECID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n LEFT JOIN [dbo].[NUMBERSEQUENCESCOPE] sc ON sc.[RECID] = n.[NUMBERSEQUENCESCOPE] ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO

View file

@ -1,130 +0,0 @@
-- ===================================================================================================================
-- SQLI-AX-009: Dynamics AX RecId er ved at løbe tør
-- ===================================================================================================================
-- Regel-id: SQLI-AX-009
-- Titel: Dynamics AX RecId er ved at løbe tør
-- Alvor: Axapta 3.0 (32-bit): High fra advarselsgrænsen, Critical fra den kritiske grænse; 64-bit: High; altid Critical, når RecId er opbrugt.
-- Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema (docs/dynamics-ax/
-- schema-ax30.tsv). AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- Når en tabels RecId-serie er brugt op, kan AX ikke oprette flere poster. Axapta 3.0 har 32-bit RecId (int, højst
-- 2.147.483.647 pr. tabel og firma), og det løses kun med et stort projekt - derfor advares langt tidligere for 3.0.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- Grænse = typens maksimum (int, når SYSTEMSEQUENCES.NEXTVAL er int som i Axapta 3.0; ellers bigint), eller MAXVAL,
-- hvis den er sat lavere. Brugt % = NEXTVAL / grænse. Prognose ud fra SQLInsights egne daglige målinger af NEXTVAL
-- (insight.AxSequenceSamples). Højst 25 fund pr. database, de mest brugte først. Tal: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : Interpret(AxRecIdRow).
--
-- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi):
-- SQLInsight:DynamicsAx:RecId32WarnPercent = 50: Axapta 3.0: High fra så mange procent af int-intervallet
-- SQLInsight:DynamicsAx:RecId32CriticalPercent = 75: Axapta 3.0: Critical
-- SQLInsight:DynamicsAx:RecId32ForecastDays = 730: Axapta 3.0: High, hvis den løber tør inden for så mange dage;
-- Critical inden for halvdelen
-- SQLInsight:DynamicsAx:RecId64WarnPercent = 80: 64-bit (eller sænket MAXVAL): High
-- SQLInsight:DynamicsAx:ForecastWarnDays = 90: 64-bit: High, hvis den løber tør inden for så mange dage
-- SQLInsight:DynamicsAx:ForecastBasisDays = 30: prognosen bruger den ældste daglige måling inden for så mange dage
--
-- Rettigheder til agentens login (se db/permissions/grant-least-privilege.sql og grant-dynamics-ax-read.sql):
-- Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql; [bruger] = agentens databasebruger):
-- Axapta 3.0: GRANT SELECT ON [bmssa].[SYSTEMSEQUENCES] TO [bruger];
-- GRANT SELECT ON [bmssa].[SQLDICTIONARY] TO [bruger]; -- valgfri: tabelnavne
-- AX 4.0/2009/2012/F&O: de samme tabeller i [dbo]
-- Mangler SYSTEMSEQUENCES, er reglen "ikke målbar" med den nøjagtige GRANT for databasen. Uden GRANT ændres intet.
-- Genkendelsen: kun grant-least-privilege.sql (VIEW ANY DEFINITION; CONNECT ANY DATABASE fra 2014, ellers en
-- databasebruger: CREATE USER [agent] FOR LOGIN [agent];).
--
-- Data kommer fra (collector, fil : medlemmer, interval):
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- AxSequenceCollector
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxSequenceCollector.cs; SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs
-- Medlemmer: AxSchemaAdapter.NumberSequences(database), AxSchemaAdapter.RecIds(database), AxSql.InDatabase
-- Interval: 1 time som standard (SQLInsight:CollectorIntervals:dynamics-ax-sequences)
-- Delt script: SQL Scripts/_collectors/AxSequenceCollector.sql
-- Regelkode: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs : AxRecIdExhaustionRule
--
-- Read-only: Ja: kun SELECT med TOP og SET-sessionsindstillinger. AX-tabellerne læses i AX-databasen via
-- [db].sys.sp_executesql (sessionen skifter aldrig database) under READ COMMITTED med SET LOCK_TIMEOUT 2000
-- (SNAPSHOT, når databasen tillader det), aldrig NOLOCK; bagefter sættes agentens sessionsindstillinger
-- igen. Intet ændrer data, indstillinger eller objekter. Kun i databaser genkendt som AX og valgt til
-- overvågning.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); varianter for andre AX-versioner end din fejler eller giver ingen rækker - det er forventet.
-- AX-forespørgslerne er vist for databasen AXDB: ret [AXDB] til din AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
-- Forbindelsen: Sessionsindstillinger, agenten sætter på hver forbindelse
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner: sættes på hver forbindelse, agenten åbner
-- Kilde: SqlConnectionFactory.SessionSettings
-- Kode: src/SQLInsight.Connectivity/SqlConnectionFactory.cs : SqlConnectionFactory.SessionSettings
SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke overvågede databaser er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxSequenceCollector: RecId pr. tabel og firma i AX-databasen (valgfri AX-læseadgang)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Axapta 3.0 (skema bmssa, verificeret mod kundedata): RecId pr. tabel (SYSTEMSEQUENCES)
-- Kilde: AxSchemaAdapter.RecIds(3.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.RecIds(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [bmssa].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [bmssa].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 4.0 (skema dbo) - ikke verificeret mod kundedata: RecId pr. tabel (SYSTEMSEQUENCES)
-- Kilde: AxSchemaAdapter.RecIds(4.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.RecIds(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [dbo].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [dbo].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: RecId pr. tabel (SYSTEMSEQUENCES)
-- Kilde: AxSchemaAdapter.RecIds(2009)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.RecIds(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [dbo].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [dbo].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: RecId pr. tabel (SYSTEMSEQUENCES)
-- Kilde: AxSchemaAdapter.RecIds(2012)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.RecIds(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [dbo].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [dbo].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO

View file

@ -1,168 +0,0 @@
-- ===================================================================================================================
-- SQLI-AX-010: AX-bruger blokerer andre
-- ===================================================================================================================
-- Regel-id: SQLI-AX-010
-- Titel: AX-bruger blokerer andre
-- Alvor: Medium; High ved ventetid over 5 minutter eller mindst 10 blokerede sessioner.
-- Versioner: Blokering: SQL Server 2000 og nyere; AX-genkendelsen og dermed reglen: SQL Server 2005 og nyere.
-- Axapta 3.0/4.0: AX-brugeren kan ikke ses. AX 2009/2012/F&O: baseret på Microsofts dokumentation - ikke
-- verificeret mod kundedata.
--
-- Hvad tjekkes, og hvorfor:
-- En session i en Dynamics AX-database holder låse, som andre AX-brugere eller batchjob venter på. Alle AX-brugere går
-- gennem AOS'ens login, så SQL Server-login'et siger ikke, hvem det er. Reglen viser AX-brugeren, når SQL Server kan
-- se den.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke i SQL'en, medmindre andet er nævnt):
-- For hver head blocker, hvis egen database eller en blokeret sessions database er genkendt som AX: fund, når den
-- længste ventetid i kæden er over grænsen. AX 2009/2012/F&O: AX-brugeren og AX-sessionen læses fra CONTEXT_INFO,
-- som AOS'en sætter, når "connectioncontext" er slået til i AOS-konfigurationen (formatet er dokumenteret af
-- Microsoft for AX 2012 - ikke verificeret mod kundedata). Axapta 3.0 og AX 4.0 sætter aldrig CONTEXT_INFO:
-- brugeren kan ikke ses fra SQL Server, og fundet siger det ærligt. AX-bruger, vært, program og CONTEXT_INFO er
-- følsomme målinger (sendes ikke til SQLInsight Hub uden SendSensitiveEvidence). Brugeren findes af
-- src/SQLInsight.Core/DynamicsAx/AxSessionMapper.cs. AX-tabeller læses ikke (og aldrig med NOLOCK).
--
-- Grænseværdier (konfigurationsnøgle i agentens appsettings.json og standardværdi):
-- SQLInsight:DynamicsAx:BlockingSeconds = 30: den længste ventetid bag sessionen skal være mindst så mange sekunder.
-- High fra 300.000 ms eller 10 blokerede (fast i koden, som SQLI-BLK-001).
--
-- Rettigheder til agentens login (se db/permissions/grant-least-privilege.sql og grant-dynamics-ax-read.sql):
-- Ingen AX-læseadgang: kun grant-least-privilege.sql.
-- GRANT VIEW SERVER STATE TO [agent]; -- SQL Server 2005-2019 (sessionerne)
-- GRANT VIEW SERVER PERFORMANCE STATE TO [agent]; -- SQL Server 2022+
-- GRANT VIEW ANY DEFINITION TO [agent]; -- AX-genkendelsen (metadata)
-- GRANT CONNECT ANY DATABASE TO [agent]; -- SQL Server 2014+; før 2014 en databasebruger i AX-databasen
--
-- Data kommer fra (collector, fil : medlemmer, interval):
-- AxDatabaseDetector (den fælles AX-genkendelse)
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs
-- Medlemmer: AxDatabaseDetector.DatabasesSql, AxDatabaseDetector.BuildDetectionSql(database)
-- Interval: hver time pr. database (hvert 5. minut, mens AX-læseadgangen mangler), delt af alle AX-collectors
-- Delt script: SQL Scripts/_collectors/AxDatabaseDetector.sql
-- BlockingCollector
-- Fil: src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs
-- Medlemmer: BlockingCollector.BuildSql2005(version), BlockingCollector.Sql2000
-- Interval: 1 min som standard (SQLInsight:CollectorIntervals:blocking)
-- Delt script: SQL Scripts/_collectors/BlockingCollector.sql
-- Regelkode: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs : AxBlockingUserRule
--
-- Read-only: Ja: kun SELECT med TOP og SET-sessionsindstillinger. AX-tabellerne læses i AX-databasen via
-- [db].sys.sp_executesql (sessionen skifter aldrig database) under READ COMMITTED med SET LOCK_TIMEOUT 2000
-- (SNAPSHOT, når databasen tillader det), aldrig NOLOCK; bagefter sættes agentens sessionsindstillinger
-- igen. Intet ændrer data, indstillinger eller objekter. Kun i databaser genkendt som AX og valgt til
-- overvågning.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); varianter for andre AX-versioner end din fejler eller giver ingen rækker - det er forventet.
-- AX-forespørgslerne er vist for databasen AXDB: ret [AXDB] til din AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
-- Forbindelsen: Sessionsindstillinger, agenten sætter på hver forbindelse
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner: sættes på hver forbindelse, agenten åbner
-- Kilde: SqlConnectionFactory.SessionSettings
-- Kode: src/SQLInsight.Connectivity/SqlConnectionFactory.cs : SqlConnectionFactory.SessionSettings
SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxDatabaseDetector: Hvilke overvågede databaser er Dynamics AX (kun metadata)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- BlockingCollector: Blokerede sessioner og deres blokerere lige nu, med sætning, åbne transaktioner og CONTEXT_INFO.
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: SQL Server 2012 og nyere
-- Kilde: BlockingCollector.BuildSql2005(2012+)
-- Kode: src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs : BlockingCollector.BuildSql2005(version)
SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.status), r.wait_type, r.wait_time, r.wait_resource,
DB_NAME(COALESCE(r.database_id, s.database_id)), s.login_name, s.host_name, s.program_name, s.open_transaction_count,
LEFT(CASE WHEN r.sql_handle IS NOT NULL AND r.statement_start_offset IS NOT NULL
THEN SUBSTRING(t.text, r.statement_start_offset / 2 + 1,
(CASE WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END
- r.statement_start_offset) / 2 + 1)
ELSE t.text END, 4000),
s.last_request_end_time, s.context_info
FROM sys.dm_exec_sessions s
LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id
LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.session_id
OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, c.most_recent_sql_handle)) t
WHERE r.blocking_session_id > 0
OR s.session_id IN (SELECT blocking_session_id FROM sys.dm_exec_requests WHERE blocking_session_id > 0)
GO
-- Variant: SQL Server 2008 – 2008 R2
-- Kilde: BlockingCollector.BuildSql2005(2008)
-- Kode: src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs : BlockingCollector.BuildSql2005(version)
SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.status), r.wait_type, r.wait_time, r.wait_resource,
DB_NAME(r.database_id), s.login_name, s.host_name, s.program_name, (SELECT COUNT(*) FROM sys.dm_tran_session_transactions st WHERE st.session_id = s.session_id),
LEFT(CASE WHEN r.sql_handle IS NOT NULL AND r.statement_start_offset IS NOT NULL
THEN SUBSTRING(t.text, r.statement_start_offset / 2 + 1,
(CASE WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END
- r.statement_start_offset) / 2 + 1)
ELSE t.text END, 4000),
s.last_request_end_time, s.context_info
FROM sys.dm_exec_sessions s
LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id
LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.session_id
OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, c.most_recent_sql_handle)) t
WHERE r.blocking_session_id > 0
OR s.session_id IN (SELECT blocking_session_id FROM sys.dm_exec_requests WHERE blocking_session_id > 0)
GO
-- Variant: SQL Server 2005 (CONTEXT_INFO fra sys.sysprocesses)
-- Kilde: BlockingCollector.BuildSql2005(2005)
-- Kode: src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs : BlockingCollector.BuildSql2005(version)
SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.status), r.wait_type, r.wait_time, r.wait_resource,
DB_NAME(r.database_id), s.login_name, s.host_name, s.program_name, (SELECT COUNT(*) FROM sys.dm_tran_session_transactions st WHERE st.session_id = s.session_id),
LEFT(CASE WHEN r.sql_handle IS NOT NULL AND r.statement_start_offset IS NOT NULL
THEN SUBSTRING(t.text, r.statement_start_offset / 2 + 1,
(CASE WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END
- r.statement_start_offset) / 2 + 1)
ELSE t.text END, 4000),
s.last_request_end_time, (SELECT TOP 1 p.context_info FROM sys.sysprocesses p WHERE p.spid = s.session_id AND p.ecid = 0)
FROM sys.dm_exec_sessions s
LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id
LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.session_id
OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, c.most_recent_sql_handle)) t
WHERE r.blocking_session_id > 0
OR s.session_id IN (SELECT blocking_session_id FROM sys.dm_exec_requests WHERE blocking_session_id > 0)
GO
-- Variant: SQL Server 2000 (legacy ODBC-sti)
-- Kilde: BlockingCollector.Sql2000
-- Kode: src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs : BlockingCollector.Sql2000
SELECT p.spid, NULLIF(CASE WHEN p.blocked = p.spid THEN 0 ELSE p.blocked END, 0), RTRIM(p.status), RTRIM(p.lastwaittype),
p.waittime, RTRIM(p.waitresource), DB_NAME(p.dbid), RTRIM(p.loginame), RTRIM(p.hostname), RTRIM(p.program_name),
p.open_tran, CAST(NULL AS nvarchar(4000)), p.last_batch, p.context_info
FROM master.dbo.sysprocesses p
WHERE (p.blocked > 0 AND p.blocked <> p.spid)
OR p.spid IN (SELECT blocked FROM master.dbo.sysprocesses WHERE blocked > 0 AND blocked <> spid)
GO

View file

@ -62,7 +62,7 @@ SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.stat
(CASE WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END
- r.statement_start_offset) / 2 + 1)
ELSE t.text END, 4000),
s.last_request_end_time, s.context_info
s.last_request_end_time
FROM sys.dm_exec_sessions s
LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id
LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.session_id
@ -71,26 +71,7 @@ WHERE r.blocking_session_id > 0
OR s.session_id IN (SELECT blocking_session_id FROM sys.dm_exec_requests WHERE blocking_session_id > 0)
GO
-- Variant: SQL Server 2008 – 2008 R2
-- Kilde: BlockingCollector.BuildSql2005(2008)
-- Kode: src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs : BlockingCollector.BuildSql2005(version)
SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.status), r.wait_type, r.wait_time, r.wait_resource,
DB_NAME(r.database_id), s.login_name, s.host_name, s.program_name, (SELECT COUNT(*) FROM sys.dm_tran_session_transactions st WHERE st.session_id = s.session_id),
LEFT(CASE WHEN r.sql_handle IS NOT NULL AND r.statement_start_offset IS NOT NULL
THEN SUBSTRING(t.text, r.statement_start_offset / 2 + 1,
(CASE WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END
- r.statement_start_offset) / 2 + 1)
ELSE t.text END, 4000),
s.last_request_end_time, s.context_info
FROM sys.dm_exec_sessions s
LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id
LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.session_id
OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, c.most_recent_sql_handle)) t
WHERE r.blocking_session_id > 0
OR s.session_id IN (SELECT blocking_session_id FROM sys.dm_exec_requests WHERE blocking_session_id > 0)
GO
-- Variant: SQL Server 2005 (CONTEXT_INFO fra sys.sysprocesses)
-- Variant: SQL Server 2005 – 2008 R2
-- Kilde: BlockingCollector.BuildSql2005(2005)
-- Kode: src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs : BlockingCollector.BuildSql2005(version)
SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.status), r.wait_type, r.wait_time, r.wait_resource,
@ -100,7 +81,7 @@ SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.stat
(CASE WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END
- r.statement_start_offset) / 2 + 1)
ELSE t.text END, 4000),
s.last_request_end_time, (SELECT TOP 1 p.context_info FROM sys.sysprocesses p WHERE p.spid = s.session_id AND p.ecid = 0)
s.last_request_end_time
FROM sys.dm_exec_sessions s
LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id
LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.session_id
@ -114,7 +95,7 @@ GO
-- Kode: src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs : BlockingCollector.Sql2000
SELECT p.spid, NULLIF(CASE WHEN p.blocked = p.spid THEN 0 ELSE p.blocked END, 0), RTRIM(p.status), RTRIM(p.lastwaittype),
p.waittime, RTRIM(p.waitresource), DB_NAME(p.dbid), RTRIM(p.loginame), RTRIM(p.hostname), RTRIM(p.program_name),
p.open_tran, CAST(NULL AS nvarchar(4000)), p.last_batch, p.context_info
p.open_tran, CAST(NULL AS nvarchar(4000)), p.last_batch
FROM master.dbo.sysprocesses p
WHERE (p.blocked > 0 AND p.blocked <> p.spid)
OR p.spid IN (SELECT blocked FROM master.dbo.sysprocesses WHERE blocked > 0 AND blocked <> spid)

View file

@ -31,8 +31,11 @@
-- Delt script: SQL Scripts/_collectors/DeadlockCollector.sql
-- Regelkode: src/SQLInsight.Rules/Concurrency/ConcurrencyRules.cs : DeadlockRule.Evaluate
--
-- Read-only: Ja: kun SELECT (sys.fn_xe_file_target_read_file og DMV'er) samt SET-sessionsindstillinger. Ingen
-- temp-tabeller; læsepositionen findes med ROW_NUMBER() i samme forespørgsel.
-- Read-only: IKKE STRENGT READ-ONLY. DeadlockCollector.BuildFileSql (SQL Server 2012+) opretter en midlertidig
-- tabel i tempdb (SELECT ... INTO #system_health) og sletter den igen (DROP TABLE #system_health) i
-- samme batch. Den er privat for sessionen; ingen brugerdata, indstillinger eller permanente objekter
-- ændres, men det er en skrivning i tempdb. Ring buffer-varianten (2008/2008 R2) er read-only. Øvrigt:
-- kun SELECT samt SET-sessionsindstillinger.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); en variant for en anden SQL Server-version end din kan fejle - det er forventet.
-- Per-database-forespørgsler er vist for databasen msdb, som findes overalt: ret navnet til den
@ -56,14 +59,14 @@ GO
-- Variant: SQL Server 2012 og nyere (system_health-filer; første læsning)
-- Kilde: DeadlockCollector.BuildFileSql
-- Kode: src/SQLInsight.Collectors/Concurrency/DeadlockCollector.cs : DeadlockCollector.BuildFileSql(lastFile: null, lastOffset: null)
SELECT e.event_data, e.file_name, e.file_offset, e.rn
FROM (
SELECT CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data,
file_name, file_offset,
ROW_NUMBER() OVER (ORDER BY file_name DESC, file_offset DESC) AS rn
FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL)
) AS e
WHERE e.event_data IS NOT NULL OR e.rn = 1;
SELECT object_name, file_name, file_offset,
CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data
INTO #system_health
FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL);
SELECT event_data FROM #system_health WHERE event_data IS NOT NULL;
SELECT TOP (1) file_name, file_offset FROM #system_health ORDER BY file_name DESC, file_offset DESC;
DROP TABLE #system_health;
GO
-- Variant: SQL Server 2008 – 2008 R2, og når event-filerne ikke kan læses

View file

@ -1,91 +0,0 @@
-- ===================================================================================================================
-- SQLI-SYN-001: Et syntetisk tjek fejler
-- ===================================================================================================================
-- Regel-id: SQLI-SYN-001
-- Titel: Et syntetisk tjek fejler
-- Alvor: High.
-- Versioner: SQL: SQL Server 2005 og nyere (SqlClient, ODBC-fallback for 2005). AX-logon: AX 2009 (kræver Windows PowerShell
-- 2.0-motoren) og AX 2012. Windows-tjenester: Windows Server 2008 R2 og nyere.
--
-- Hvad tjekkes, og hvorfor:
-- Agenten gør det, en bruger eller applikation ville gøre – forbinder til en port, henter en side, logger på SQL Server
-- og kører en forespørgsel, slår et navn op, logger på Dynamics AX – og det fejler. Er tjekket knyttet til en tjeneste
-- på en Windows-server, og kører tjenesten, siges det direkte: "… svarer ikke, selv om tjenesten kører" – den fejl,
-- en tjenesteovervågning alene ikke ser.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- Har de seneste FailuresBeforeFinding kørsler alle fejlet, er det et fund. Titlen siger, om den tilknyttede tjeneste
-- kører, er stoppet (eller i en anden tilstand) eller ikke findes, eller at tjekket fejler N gange i træk. Fejlteksten
-- står aldrig i titlen (den sendes uredigeret til API og notifikationer), kun som måling (følsom). Er alle de
-- resultater, agenten har givet reglen, fejl, siges "mindst N" gange, og "Fejler siden" er "… eller tidligere".
--
-- Grænseværdier (agentens appsettings.json og tjekkets indstillinger):
-- SQLInsight:SyntheticChecks:FailuresBeforeFinding standard 2 (fejl i træk før et fund: ét tabt svar er ingen alarm)
-- Pr. tjek: interval (standard 300 s, 60–86400) og timeout (standard 15 s, AX 60 s, højst 120).
--
-- Rettigheder:
-- TCP, HTTP(S), DNS: ingen ud over netværksadgang fra agentens server. HTTP med Windows-godkendelse: agentens konto.
-- SQL: login'et i tjekket (agentens Windows-konto eller et SQL-login, hvis adgangskode krypteres til agenten som i 1.3)
-- skal kunne logge ind og køre forespørgslen; least-privilege-login'et fra grant-least-privilege.sql er nok til standardforespørgslen.
-- AX-logon: agentens Windows-konto skal være AX-bruger (i firmaet), og .NET Business Connector skal være installeret på
-- agentens server; ellers testes kun AOS-porten (og resultatet siger det).
-- Tjenestens tilstand: WMI Remote Enable på root\cimv2 + læseret på Service Control Manager på tjenestens server.
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/Synthetic/SyntheticCheckRunner.cs – SyntheticCheckRunner.RunAsync (TcpAsync, HttpAsync, SqlAsync,
-- DnsAsync, AxAsync), AX-logon: src/SQLInsight.Servers/Synthetic/AxBusinessConnector.cs og ax-logon.ps1. Køres efter
-- tjekkets interval af src/SQLInsight.Agent/Servers/SyntheticCheckService.cs – RunCheckAsync, som også slår den tilknyttede
-- tjenestes tilstand op (CimQueries.SingleServiceQuery).
-- Regelkode: src/SQLInsight.Rules/Servers/SyntheticCheckRules.cs : SyntheticCheckFailingRule
--
-- Read-only: Ja. Ved et SQL-tjek valideres forespørgslen som én skrivebeskyttet SELECT, både når tjekket gemmes og
-- før hver kørsel (src/SQLInsight.Core/CustomRules/ReadOnlyQuery.cs), og den køres i en transaktion, der
-- altid rulles tilbage. Andre typer tjek sender ingen SQL og ændrer intet.
-- Kørsel: Kør i SSMS som tjekkets login. Hver GO er en separat kommando på samme forbindelse; ret [msdb] til
-- tjekkets database, eller udelad USE, hvis tjekket ingen database har.
-- ===================================================================================================================
-- SQL: Ved et SQL-tjek kører agenten præcis dette på den instans, tjekket peger på (ændrer intet: forespørgslen
-- valideres som én skrivebeskyttet SELECT, både når den gemmes og før den køres, og køres i en transaktion, der altid
-- rulles tilbage). Hver GO er en separat kommando på samme forbindelse.
-- -------------------------------------------------------------------------------------------------------------------
-- SyntheticCheckRunner: SQL-tjekket (login, sessionsindstillinger, forespørgslen i en transaktion, der rulles tilbage)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner: sættes på hver forbindelse, agenten åbner (også tjekkets)
-- Kilde: SqlConnectionFactory.SessionSettings
-- Kode: src/SQLInsight.Connectivity/SqlConnectionFactory.cs : SqlConnectionFactory.SessionSettings
SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: Kun når tjekket har en database (vist for msdb)
-- Kilde: SyntheticCheckRunner.UseDatabaseSql
-- Kode: src/SQLInsight.Servers/Synthetic/SyntheticCheckRunner.cs : SyntheticCheckRunner.UseDatabaseSql(database)
USE [msdb];
GO
-- Variant: Alle versioner fra SQL Server 2005: før forespørgslen
-- Kilde: SyntheticCheckRunner.BeginTransactionSql
-- Kode: src/SQLInsight.Servers/Synthetic/SyntheticCheckRunner.cs : SyntheticCheckRunner.BeginTransactionSql
BEGIN TRANSACTION;
GO
-- Variant: Standardforespørgslen, når tjekket ikke har sin egen (en egen forespørgsel køres her i stedet, højst 10.000 rækker læses)
-- Kilde: SyntheticCheckSettings.DefaultQuery
-- Kode: src/SQLInsight.Core/SyntheticChecks/SyntheticCheckDefinitions.cs : SyntheticCheckSettings.DefaultQuery
SELECT 1
GO
-- Variant: Alle versioner fra SQL Server 2005: altid efter forespørgslen, også når den fejler
-- Kilde: SyntheticCheckRunner.RollbackSql
-- Kode: src/SQLInsight.Servers/Synthetic/SyntheticCheckRunner.cs : SyntheticCheckRunner.RollbackSql
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
GO
-- Ikke-SQL-tjek, efterprøv med PowerShell (skrivebeskyttet; kør på agentens server som agentens konto):
-- TCP: Test-NetConnection -ComputerName aos01 -Port 2712
-- HTTP: Invoke-WebRequest -Uri https://ep.firma.local/ -UseDefaultCredentials -UseBasicParsing | Select-Object StatusCode
-- DNS: Resolve-DnsName aos01.firma.local
-- AX: Test-NetConnection -ComputerName aos01 -Port 2712 (logon: se src/SQLInsight.Servers/Synthetic/ax-logon.ps1)
-- Tjeneste: Get-CimInstance -ComputerName aos01 -Query "SELECT Name, DisplayName, State, StartMode, ExitCode FROM Win32_Service WHERE Name = 'AOS60`$01'"

View file

@ -1,73 +0,0 @@
-- ===================================================================================================================
-- SQLI-SYN-002: Et syntetisk tjek svarer langsomt
-- ===================================================================================================================
-- Regel-id: SQLI-SYN-002
-- Titel: Et syntetisk tjek svarer langsomt
-- Alvor: Medium.
-- Versioner: Som SQLI-SYN-001.
--
-- Hvad tjekkes, og hvorfor:
-- Tjekket lykkes, men svaret har de seneste gange taget længere end tjekkets grænse. Brugerne oplever det som en langsom
-- applikation, længe før den holder op med at svare.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- Er de seneste SlowRunsBeforeFinding kørsler alle lykkedes og alle langsommere end grænsen, er det et fund.
--
-- Grænseværdier (agentens appsettings.json og tjekkets indstillinger):
-- Pr. tjek: "Langsomt svar over (ms)" (insight.SyntheticChecks.SettingsJson: warnMilliseconds). Uden grænse: "ikke relevant".
-- SQLInsight:SyntheticChecks:SlowRunsBeforeFinding standard 3 (langsomme svar i træk før et fund)
--
-- Rettigheder:
-- Som SQLI-SYN-001.
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/Synthetic/SyntheticCheckRunner.cs – SyntheticCheckRunner.RunAsync (DurationMs: hele tjekket, for
-- SQL login + forespørgsel), fra src/SQLInsight.Agent/Servers/SyntheticCheckService.cs – RunCheckAsync.
-- Regelkode: src/SQLInsight.Rules/Servers/SyntheticCheckRules.cs : SyntheticCheckSlowRule
--
-- Read-only: Ja. Ved et SQL-tjek valideres forespørgslen som én skrivebeskyttet SELECT, både når tjekket gemmes og
-- før hver kørsel (src/SQLInsight.Core/CustomRules/ReadOnlyQuery.cs), og den køres i en transaktion, der
-- altid rulles tilbage. Andre typer tjek sender ingen SQL og ændrer intet.
-- Kørsel: Kør i SSMS som tjekkets login. Hver GO er en separat kommando på samme forbindelse; ret [msdb] til
-- tjekkets database, eller udelad USE, hvis tjekket ingen database har.
-- ===================================================================================================================
-- SQL: Et SQL-tjek kører præcis det samme som i SQLI-SYN-001 (svartiden er login plus forespørgsel):
-- -------------------------------------------------------------------------------------------------------------------
-- SyntheticCheckRunner: SQL-tjekket (login, sessionsindstillinger, forespørgslen i en transaktion, der rulles tilbage)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Alle versioner: sættes på hver forbindelse, agenten åbner (også tjekkets)
-- Kilde: SqlConnectionFactory.SessionSettings
-- Kode: src/SQLInsight.Connectivity/SqlConnectionFactory.cs : SqlConnectionFactory.SessionSettings
SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: Kun når tjekket har en database (vist for msdb)
-- Kilde: SyntheticCheckRunner.UseDatabaseSql
-- Kode: src/SQLInsight.Servers/Synthetic/SyntheticCheckRunner.cs : SyntheticCheckRunner.UseDatabaseSql(database)
USE [msdb];
GO
-- Variant: Alle versioner fra SQL Server 2005: før forespørgslen
-- Kilde: SyntheticCheckRunner.BeginTransactionSql
-- Kode: src/SQLInsight.Servers/Synthetic/SyntheticCheckRunner.cs : SyntheticCheckRunner.BeginTransactionSql
BEGIN TRANSACTION;
GO
-- Variant: Standardforespørgslen, når tjekket ikke har sin egen (en egen forespørgsel køres her i stedet, højst 10.000 rækker læses)
-- Kilde: SyntheticCheckSettings.DefaultQuery
-- Kode: src/SQLInsight.Core/SyntheticChecks/SyntheticCheckDefinitions.cs : SyntheticCheckSettings.DefaultQuery
SELECT 1
GO
-- Variant: Alle versioner fra SQL Server 2005: altid efter forespørgslen, også når den fejler
-- Kilde: SyntheticCheckRunner.RollbackSql
-- Kode: src/SQLInsight.Servers/Synthetic/SyntheticCheckRunner.cs : SyntheticCheckRunner.RollbackSql
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
GO
-- Ikke-SQL-tjek, efterprøv svartiden med PowerShell (skrivebeskyttet):
-- Measure-Command { Invoke-WebRequest -Uri https://ep.firma.local/ -UseDefaultCredentials -UseBasicParsing } | Select-Object TotalMilliseconds
-- Measure-Command { Test-NetConnection -ComputerName aos01 -Port 2712 } | Select-Object TotalMilliseconds

View file

@ -1,38 +0,0 @@
-- ===================================================================================================================
-- SQLI-SYN-003: TLS-certifikatet udløber snart
-- ===================================================================================================================
-- Regel-id: SQLI-SYN-003
-- Titel: TLS-certifikatet udløber snart
-- Alvor: Medium; High inden for CertificateCriticalDays; Critical når det er udløbet.
-- Versioner: Alle HTTPS-servere (TLS 1.2 og nyere fra agentens server).
--
-- Hvad tjekkes, og hvorfor:
-- Når certifikatet på en HTTPS-adresse udløber, afviser browsere og integrationer forbindelsen (eller viser en
-- sikkerhedsadvarsel), og siden er i praksis nede. Udløbsdatoen læses ved hvert HTTPS-tjek – også når certifikatet
-- afvises (fx selvsigneret).
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- Kun HTTP-tjek med https://. Den nyeste kørsel, der har set et certifikat, bruges: dage til udløb = NotAfter − nu.
-- Uden set certifikat endnu: "ikke målbar". Andre tjek: "ikke relevant".
--
-- Grænseværdier (agentens appsettings.json):
-- SQLInsight:SyntheticChecks:CertificateWarningDays standard 21 (fund når certifikatet udløber inden for så mange dage)
-- SQLInsight:SyntheticChecks:CertificateCriticalDays standard 7 (Høj inden for så mange dage)
--
-- Rettigheder:
-- Ingen ud over netværksadgang fra agentens server til adressen.
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/Synthetic/SyntheticCheckRunner.cs – HttpAsync (RemoteCertificateValidationCallback gemmer
-- certifikatets NotAfter og Subject), fra src/SQLInsight.Agent/Servers/SyntheticCheckService.cs – RunCheckAsync.
-- Regelkode: src/SQLInsight.Rules/Servers/SyntheticCheckRules.cs : CertificateExpiryRule
--
-- Read-only: Ja. Reglen sender ingen SQL og ændrer intet; filen indeholder kun kommentarer.
-- ===================================================================================================================
-- Reglen bruger ikke SQL. Tjekket er en HTTPS GET; filen indeholder kun kommentarer og ændrer intet.
--
-- Efterprøv med PowerShell (skrivebeskyttet; kør på agentens server):
-- $c = New-Object System.Net.Sockets.TcpClient('ep.firma.local', 443)
-- $s = New-Object System.Net.Security.SslStream($c.GetStream(), $false, { $true })
-- $s.AuthenticateAsClient('ep.firma.local'); $s.RemoteCertificate.GetExpirationDateString(); $c.Close()

View file

@ -1,50 +0,0 @@
-- ===================================================================================================================
-- SQLI-WIN-001: CPU-forbruget er højt i længere tid
-- ===================================================================================================================
-- Regel-id: SQLI-WIN-001
-- Titel: CPU-forbruget er højt i længere tid
-- Alvor: High; Critical når gennemsnittet er 98 % eller derover.
-- Versioner: Windows Server 2008 R2 og nyere (WS-Man fra Windows Server 2012 / WinRM 3.0; DCOM på alle).
--
-- Hvad tjekkes, og hvorfor:
-- Serverens processorer har i gennemsnit været næsten fuldt belastet i en længere periode. Alt på serveren bliver
-- langsommere (forespørgsler, AOS-kald, fildeling, logon). Korte toppe er normale og giver intet fund.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- Hvert minut læses tælleren Processor(_Total)\% Processor Time som rå værdi. CPU for minuttet er
-- 100 × (1 − Δ tomgangstid / Δ tid) mellem to læsninger (PERF_100NSEC_TIMER_INV), altså gennemsnittet over hele
-- minuttet, ikke et øjebliksbillede. Er gennemsnittet af minutterne i de seneste SustainedMinutes minutter mindst
-- HighPercent, er det et fund. Uden to læsninger endnu: "ikke målbar". Er den forrige læsning mere end tre intervaller
-- gammel (serveren kunne ikke nås et stykke tid), giver minuttet ingen CPU-værdi frem for et gennemsnit af hele hullet.
--
-- Grænseværdier (agentens appsettings.json):
-- SQLInsight:Servers:Cpu:HighPercent standard 90 (gennemsnitlig CPU i procent)
-- SQLInsight:Servers:Cpu:SustainedMinutes standard 15 (perioden gennemsnittet tages over)
-- Målingerne skal dække mindst 80 % af perioden (en kort top efter et hul tæller ikke).
--
-- Rettigheder på den overvågede server (db/permissions/grant-windows-server.ps1, aldrig lokal administrator):
-- Performance Monitor Users (ydelsestællere) + WMI Enable Account/Remote Enable på root\cimv2
-- WS-Man: Remote Management Users · DCOM: Distributed COM Users
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/CimWindowsServerSource.cs – Session.GetPerformanceAsync (CimQueries.ProcessorQuery),
-- hvert minut (SQLInsight:Servers:Intervals:performance) fra src/SQLInsight.Agent/Servers/ServerMonitor.cs – CollectPerformanceAsync.
-- Beregning: src/SQLInsight.Core/Servers/ServerPerformance.cs – ServerPerformanceMath.Cpu.
-- Regelkode: src/SQLInsight.Rules/Servers/WindowsServerRules.cs : ServerCpuRule
--
-- Read-only: Ja. Reglen sender ingen SQL. Agenten læser kun fra Windows: CIM/WQL-forespørgsler (SELECT),
-- hændelsesloggen og for planlagte opgaver Task Schedulers skrivebeskyttede GetInfo. Den starter, stopper
-- eller ændrer intet på serveren. Filen indeholder kun kommentarer; forespørgslerne mod Windows står herunder.
-- ===================================================================================================================
-- Reglen bruger ikke SQL. Forespørgslerne mod Windows står herunder; filen indeholder kun kommentarer og ændrer intet.
--
-- CIM/WQL (namespace root\cimv2):
-- SELECT PercentProcessorTime, Timestamp_Sys100NS FROM Win32_PerfRawData_PerfOS_Processor WHERE Name = '_Total'
-- SELECT NumberOfLogicalProcessors FROM Win32_ComputerSystem
--
-- Efterprøv med PowerShell (skrivebeskyttet; kør på agentens server som agentens konto):
-- $s = New-CimSession -ComputerName SERVER # DCOM: -SessionOption (New-CimSessionOption -Protocol Dcom)
-- $q = "SELECT PercentProcessorTime, Timestamp_Sys100NS FROM Win32_PerfRawData_PerfOS_Processor WHERE Name = '_Total'"
-- $a = Get-CimInstance -CimSession $s -Query $q; Start-Sleep -Seconds 60; $b = Get-CimInstance -CimSession $s -Query $q
-- 100 * (1 - ($b.PercentProcessorTime - $a.PercentProcessorTime) / ($b.Timestamp_Sys100NS - $a.Timestamp_Sys100NS))

View file

@ -1,48 +0,0 @@
-- ===================================================================================================================
-- SQLI-WIN-002: Serveren har næsten ingen ledig hukommelse
-- ===================================================================================================================
-- Regel-id: SQLI-WIN-002
-- Titel: Serveren har næsten ingen ledig hukommelse
-- Alvor: Medium; High under 2 % og under 256 MB.
-- Versioner: Windows Server 2008 R2 og nyere.
--
-- Hvad tjekkes, og hvorfor:
-- Når Windows løber tør for ledig hukommelse, flyttes data til sidefilen, alt bliver langsomt, og processer kan fejle
-- med "out of memory". SQL Server kan blive tvunget til at give hukommelse tilbage.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- Ledig hukommelse er Memory\Available MBytes; andelen er den i forhold til Win32_OperatingSystem.TotalVisibleMemorySize.
-- Gennemsnittet over de seneste 5 minutter (mindst 80 % dækket) sammenlignes med begge grænser.
--
-- Grænseværdier (agentens appsettings.json):
-- SQLInsight:Servers:Memory:AvailablePercent standard 5 (andel ledig af den samlede hukommelse)
-- SQLInsight:Servers:Memory:AvailableMegabytes standard 1024 (ledig hukommelse i MB)
-- Begge skal være under grænsen (gennemsnit over 5 minutter), så store SQL Server-hosts, der bevidst har lidt
-- tilbage, men stadig flere GB, ikke giver fund.
--
-- Rettigheder på den overvågede server (db/permissions/grant-windows-server.ps1, aldrig lokal administrator):
-- Performance Monitor Users + WMI Enable Account/Remote Enable på root\cimv2
-- WS-Man: Remote Management Users · DCOM: Distributed COM Users
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/CimWindowsServerSource.cs – Session.GetPerformanceAsync (CimQueries.MemoryQuery) og
-- Session.GetOperatingSystemAsync (CimQueries.OperatingSystemQuery), hvert minut fra
-- src/SQLInsight.Agent/Servers/ServerMonitor.cs – CollectPerformanceAsync.
-- Regelkode: src/SQLInsight.Rules/Servers/WindowsServerRules.cs : ServerMemoryRule
--
-- Read-only: Ja. Reglen sender ingen SQL. Agenten læser kun fra Windows: CIM/WQL-forespørgsler (SELECT),
-- hændelsesloggen og for planlagte opgaver Task Schedulers skrivebeskyttede GetInfo. Den starter, stopper
-- eller ændrer intet på serveren. Filen indeholder kun kommentarer; forespørgslerne mod Windows står herunder.
-- ===================================================================================================================
-- Reglen bruger ikke SQL. Forespørgslerne mod Windows står herunder; filen indeholder kun kommentarer og ændrer intet.
--
-- CIM/WQL (namespace root\cimv2):
-- SELECT AvailableMBytes, PagesPersec, CommittedBytes, CommitLimit, Timestamp_PerfTime, Frequency_PerfTime FROM Win32_PerfRawData_PerfOS_Memory
-- SELECT Caption, Version, BuildNumber, LastBootUpTime, TotalVisibleMemorySize, CSName FROM Win32_OperatingSystem
--
-- Efterprøv med PowerShell (skrivebeskyttet; kør på agentens server som agentens konto):
-- $s = New-CimSession -ComputerName SERVER
-- $m = Get-CimInstance -CimSession $s -Query "SELECT AvailableMBytes FROM Win32_PerfRawData_PerfOS_Memory"
-- $o = Get-CimInstance -CimSession $s -Query "SELECT TotalVisibleMemorySize FROM Win32_OperatingSystem"
-- "{0} MB ledig ({1:N1} %)" -f $m.AvailableMBytes, (100 * $m.AvailableMBytes * 1024 / $o.TotalVisibleMemorySize)

View file

@ -1,46 +0,0 @@
-- ===================================================================================================================
-- SQLI-WIN-003: Serveren pager hukommelse til disk
-- ===================================================================================================================
-- Regel-id: SQLI-WIN-003
-- Titel: Serveren pager hukommelse til disk
-- Alvor: Medium.
-- Versioner: Windows Server 2008 R2 og nyere.
--
-- Hvad tjekkes, og hvorfor:
-- Windows læser og skriver hukommelsessider til sidefilen, fordi der ikke er plads i RAM. Det er mange gange
-- langsommere end hukommelse og belaster disken, som andre også bruger.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- Pages/sec er en PERF_COUNTER_COUNTER: (Δ PagesPersec) / (Δ Timestamp_PerfTime / Frequency_PerfTime) mellem to
-- læsninger. Er gennemsnittet over 15 minutter mindst grænsen, og den ledige andel samtidig under 10 %, er det et fund.
--
-- Grænseværdier (agentens appsettings.json):
-- SQLInsight:Servers:Memory:PagesPerSecond standard 1000 (Memory\Pages/sec, gennemsnit over 15 minutter)
-- Kun sammen med lidt ledig hukommelse (under 10 % i samme periode): Pages/sec alene er også fil-I/O gennem
-- memory-mapped filer og er ikke i sig selv et problem.
--
-- Rettigheder på den overvågede server (db/permissions/grant-windows-server.ps1, aldrig lokal administrator):
-- Performance Monitor Users + WMI Enable Account/Remote Enable på root\cimv2
-- WS-Man: Remote Management Users · DCOM: Distributed COM Users
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/CimWindowsServerSource.cs – Session.GetPerformanceAsync (CimQueries.MemoryQuery), hvert minut
-- fra src/SQLInsight.Agent/Servers/ServerMonitor.cs – CollectPerformanceAsync.
-- Beregning: src/SQLInsight.Core/Servers/ServerPerformance.cs – ServerPerformanceMath.PagesPerSecond.
-- Regelkode: src/SQLInsight.Rules/Servers/WindowsServerRules.cs : ServerPagingRule
--
-- Read-only: Ja. Reglen sender ingen SQL. Agenten læser kun fra Windows: CIM/WQL-forespørgsler (SELECT),
-- hændelsesloggen og for planlagte opgaver Task Schedulers skrivebeskyttede GetInfo. Den starter, stopper
-- eller ændrer intet på serveren. Filen indeholder kun kommentarer; forespørgslerne mod Windows står herunder.
-- ===================================================================================================================
-- Reglen bruger ikke SQL. Forespørgslerne mod Windows står herunder; filen indeholder kun kommentarer og ændrer intet.
--
-- CIM/WQL (namespace root\cimv2):
-- SELECT AvailableMBytes, PagesPersec, CommittedBytes, CommitLimit, Timestamp_PerfTime, Frequency_PerfTime FROM Win32_PerfRawData_PerfOS_Memory
--
-- Efterprøv med PowerShell (skrivebeskyttet; kør på agentens server som agentens konto):
-- $s = New-CimSession -ComputerName SERVER
-- $q = "SELECT PagesPersec, Timestamp_PerfTime, Frequency_PerfTime FROM Win32_PerfRawData_PerfOS_Memory"
-- $a = Get-CimInstance -CimSession $s -Query $q; Start-Sleep -Seconds 60; $b = Get-CimInstance -CimSession $s -Query $q
-- ($b.PagesPersec - $a.PagesPersec) / (($b.Timestamp_PerfTime - $a.Timestamp_PerfTime) / $b.Frequency_PerfTime)

View file

@ -1,41 +0,0 @@
-- ===================================================================================================================
-- SQLI-WIN-004: Den reserverede hukommelse (commit) er tæt på grænsen
-- ===================================================================================================================
-- Regel-id: SQLI-WIN-004
-- Titel: Den reserverede hukommelse (commit) er tæt på grænsen
-- Alvor: Medium; High ved 97 % eller derover.
-- Versioner: Windows Server 2008 R2 og nyere.
--
-- Hvad tjekkes, og hvorfor:
-- Commit charge er den hukommelse, programmerne har reserveret (RAM plus sidefil). Når grænsen nås, kan Windows ikke
-- give mere hukommelse ud, og programmer og tjenester fejler, selv om der måske er ledig RAM.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- 100 × CommittedBytes / CommitLimit pr. minut; gennemsnittet over 5 minutter (mindst 80 % dækket) mod grænsen.
--
-- Grænseværdier (agentens appsettings.json):
-- SQLInsight:Servers:Memory:CommitPercent standard 90 (CommittedBytes / CommitLimit, gennemsnit over 5 minutter)
--
-- Rettigheder på den overvågede server (db/permissions/grant-windows-server.ps1, aldrig lokal administrator):
-- Performance Monitor Users + WMI Enable Account/Remote Enable på root\cimv2
-- WS-Man: Remote Management Users · DCOM: Distributed COM Users
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/CimWindowsServerSource.cs – Session.GetPerformanceAsync (CimQueries.MemoryQuery), hvert minut
-- fra src/SQLInsight.Agent/Servers/ServerMonitor.cs – CollectPerformanceAsync.
-- Regelkode: src/SQLInsight.Rules/Servers/WindowsServerRules.cs : ServerCommitRule
--
-- Read-only: Ja. Reglen sender ingen SQL. Agenten læser kun fra Windows: CIM/WQL-forespørgsler (SELECT),
-- hændelsesloggen og for planlagte opgaver Task Schedulers skrivebeskyttede GetInfo. Den starter, stopper
-- eller ændrer intet på serveren. Filen indeholder kun kommentarer; forespørgslerne mod Windows står herunder.
-- ===================================================================================================================
-- Reglen bruger ikke SQL. Forespørgslerne mod Windows står herunder; filen indeholder kun kommentarer og ændrer intet.
--
-- CIM/WQL (namespace root\cimv2):
-- SELECT AvailableMBytes, PagesPersec, CommittedBytes, CommitLimit, Timestamp_PerfTime, Frequency_PerfTime FROM Win32_PerfRawData_PerfOS_Memory
--
-- Efterprøv med PowerShell (skrivebeskyttet; kør på agentens server som agentens konto):
-- $s = New-CimSession -ComputerName SERVER
-- $m = Get-CimInstance -CimSession $s -Query "SELECT CommittedBytes, CommitLimit FROM Win32_PerfRawData_PerfOS_Memory"
-- 100 * $m.CommittedBytes / $m.CommitLimit

View file

@ -1,49 +0,0 @@
-- ===================================================================================================================
-- SQLI-WIN-005: Lidt ledig plads på et drev
-- ===================================================================================================================
-- Regel-id: SQLI-WIN-005
-- Titel: Lidt ledig plads på et drev
-- Alvor: Medium under FreePercent, High under CriticalFreePercent, Critical under 1 GB eller 1 %.
-- Versioner: Windows Server 2008 R2 og nyere.
--
-- Hvad tjekkes, og hvorfor:
-- Når et drev løber fuldt, stopper alt, der skriver til det: SQL Server kan ikke vokse data- og logfiler, backup
-- fejler, AOS og Windows kan ikke skrive logfiler og midlertidige filer, og brugere kan ikke gemme.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- Lokale diske (DriveType 3) med drevbogstav eller monteringssti; partitioner uden (system/recovery, navn \\?\Volume{…})
-- springes over. Ledig andel = FreeSpace / Capacity. Et fund pr. drev, der er under FreePercent og samtidig har
-- mindre end EnoughFreeGigabytes ledig.
--
-- Grænseværdier (agentens appsettings.json):
-- SQLInsight:Servers:Disks:FreePercent standard 10 (ledig andel, under: Middel)
-- SQLInsight:Servers:Disks:CriticalFreePercent standard 5 (under: Høj)
-- SQLInsight:Servers:Disks:EnoughFreeGigabytes standard 50 (er der mindst så mange GB ledige, er der intet fund,
-- uanset andelen: store drev med lille andel ledig kan være fine)
--
-- Rettigheder på den overvågede server (db/permissions/grant-windows-server.ps1, aldrig lokal administrator):
-- WMI Enable Account/Remote Enable på root\cimv2
-- WS-Man: Remote Management Users · DCOM: Distributed COM Users
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/CimWindowsServerSource.cs – Session.GetDisksAsync (CimQueries.VolumeQuery, ellers
-- CimQueries.LogicalDiskQuery), hvert 15. minut (SQLInsight:Servers:Intervals:disks) fra
-- src/SQLInsight.Agent/Servers/ServerMonitor.cs – CollectDisksAsync.
-- Regelkode: src/SQLInsight.Rules/Servers/WindowsServerRules.cs : DiskFreeSpaceRule
--
-- Read-only: Ja. Reglen sender ingen SQL. Agenten læser kun fra Windows: CIM/WQL-forespørgsler (SELECT),
-- hændelsesloggen og for planlagte opgaver Task Schedulers skrivebeskyttede GetInfo. Den starter, stopper
-- eller ændrer intet på serveren. Filen indeholder kun kommentarer; forespørgslerne mod Windows står herunder.
-- ===================================================================================================================
-- Reglen bruger ikke SQL. Forespørgslerne mod Windows står herunder; filen indeholder kun kommentarer og ændrer intet.
--
-- CIM/WQL (namespace root\cimv2):
-- SELECT Name, Label, FileSystem, Capacity, FreeSpace FROM Win32_Volume WHERE DriveType = 3
-- SELECT DeviceID, VolumeName, FileSystem, Size, FreeSpace FROM Win32_LogicalDisk WHERE DriveType = 3 (fallback)
--
-- Efterprøv med PowerShell (skrivebeskyttet; kør på agentens server som agentens konto):
-- $s = New-CimSession -ComputerName SERVER
-- Get-CimInstance -CimSession $s -Query "SELECT Name, Label, FileSystem, Capacity, FreeSpace FROM Win32_Volume WHERE DriveType = 3" |
-- Where-Object { $_.Name -notlike '\\?\*' } |
-- Select-Object Name, Label, @{n='LedigGB';e={[math]::Round($_.FreeSpace / 1GB, 1)}}, @{n='Ledig%';e={[math]::Round(100 * $_.FreeSpace / $_.Capacity, 1)}}

View file

@ -1,58 +0,0 @@
-- ===================================================================================================================
-- SQLI-WIN-006: Et drev bliver fuldt om få dage
-- ===================================================================================================================
-- Regel-id: SQLI-WIN-006
-- Titel: Et drev bliver fuldt om få dage
-- Alvor: Medium; High inden for 7 dage; Critical inden for 2 dage.
-- Versioner: Windows Server 2008 R2 og nyere.
--
-- Hvad tjekkes, og hvorfor:
-- Varsler, at disken løber fuld om et antal dage, før det sker: pladsen på drevet er de seneste uger brugt i et jævnt
-- tempo, og fortsætter det, er drevet fuldt inden for prognosegrænsen.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- Ledig plads pr. drev hver 15. minut gemmes i repositoriet. Prognosen tager timegennemsnit af de seneste
-- ForecastHistoryDays dage – kun siden drevet sidst skiftede størrelse (et udvidet drev starter en ny tendens) – og
-- lægger en ret linje (mindste kvadraters metode) gennem den ledige plads. Hælder linjen nedad, passer den rimeligt
-- (R²), og er der nok historik, er "fuld om ca. N dage" = ledig plads ved seneste måling / forbrug pr. dag, talt fra nu
-- (er seneste timegennemsnit mere end en time gammelt, trækkes resten fra). Svinger pladsen op og ned (backup skrevet og
-- slettet), er der ingen prognose frem for et skræmmende gæt. Har intet drev nok historik endnu (en ny server de første
-- par dage), er reglen "ikke målbar" med grunden – ikke "bestået".
--
-- Grænseværdier (agentens appsettings.json):
-- SQLInsight:Servers:Disks:ForecastDays standard 30 (fund når drevet forventes fuldt inden for så mange dage)
-- SQLInsight:Servers:Disks:ForecastHistoryDays standard 14 (dage med SQLInsights egne målinger, linjen beregnes over)
-- SQLInsight:Servers:DiskHistoryDays standard 35 (hvor længe målingerne gemmes i insight.ServerDiskSamples)
-- Faste fornuftsgrænser (src/SQLInsight.Core/Servers/DiskForecast.cs – DiskForecastSettings): mindst 2 dages historik og
-- 12 målinger, R² mindst 0,5, vækst mindst 0,01 GB pr. dag, højst 365 dage frem.
--
-- Rettigheder på den overvågede server (db/permissions/grant-windows-server.ps1, aldrig lokal administrator):
-- WMI Enable Account/Remote Enable på root\cimv2
-- WS-Man: Remote Management Users · DCOM: Distributed COM Users
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/CimWindowsServerSource.cs – Session.GetDisksAsync (CimQueries.VolumeQuery), hvert 15. minut fra
-- src/SQLInsight.Agent/Servers/ServerMonitor.cs – CollectDisksAsync, som gemmer målingen og beregner prognosen.
-- Prognose: src/SQLInsight.Core/Servers/DiskForecast.cs – DiskForecaster.Forecast.
-- Regelkode: src/SQLInsight.Rules/Servers/WindowsServerRules.cs : DiskForecastRule
--
-- Read-only: Ja. Reglen sender ingen SQL. Agenten læser kun fra Windows: CIM/WQL-forespørgsler (SELECT),
-- hændelsesloggen og for planlagte opgaver Task Schedulers skrivebeskyttede GetInfo. Den starter, stopper
-- eller ændrer intet på serveren. Filen indeholder kun kommentarer; forespørgslerne mod Windows står herunder.
-- ===================================================================================================================
-- Reglen bruger ikke SQL mod den overvågede server. Forespørgslen mod Windows står herunder, og den forespørgsel, agenten
-- bruger mod sit eget repository (ikke den overvågede server), står til sidst. Filen indeholder kun kommentarer.
--
-- CIM/WQL (namespace root\cimv2):
-- SELECT Name, Label, FileSystem, Capacity, FreeSpace FROM Win32_Volume WHERE DriveType = 3
--
-- Historikken i SQLInsight-repositoriet (src/SQLInsight.Repository/InsightRepository.WindowsServers.cs – GetServerDiskHistoryAsync):
-- SELECT Volume, DATEADD(hour, DATEDIFF(hour, 0, SampledAt), 0) AS [Hour], MAX(SizeGb), AVG(FreeGb)
-- FROM insight.ServerDiskSamples WHERE ServerId = @id AND SampledAt >= @from
-- GROUP BY Volume, DATEADD(hour, DATEDIFF(hour, 0, SampledAt), 0)
-- ORDER BY Volume, [Hour];
--
-- Efterprøv med PowerShell (skrivebeskyttet; ledig plads nu):
-- Get-CimInstance -ComputerName SERVER -Query "SELECT Name, Capacity, FreeSpace FROM Win32_Volume WHERE DriveType = 3" |
-- Select-Object Name, @{n='LedigGB';e={[math]::Round($_.FreeSpace / 1GB, 1)}}

View file

@ -1,54 +0,0 @@
-- ===================================================================================================================
-- SQLI-WIN-007: En tjeneste, der skal starte automatisk, kører ikke
-- ===================================================================================================================
-- Regel-id: SQLI-WIN-007
-- Titel: En tjeneste, der skal starte automatisk, kører ikke
-- Alvor: Medium.
-- Versioner: Windows Server 2008 R2 og nyere (DelayedAutoStart læses fra Windows Server 2012; før det uden).
--
-- Hvad tjekkes, og hvorfor:
-- En tjeneste med starttypen Automatisk står ikke som kørende. Det, tjenesten gør, sker ikke, og en automatisk
-- tjeneste, der er stoppet, er oftest gået ned. SQLInsight starter aldrig selv tjenester.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- Et fund pr. tjeneste med StartMode = 'Auto' og State <> 'Running', som hverken står på den indbyggede liste, i
-- Ignore, i serverens ignoreringsliste eller i serverens liste over overvågede tjenester. Listerne matcher tjenestens
-- navn eller visningsnavn, præcist eller med præfiks* – på samme måde som SQLI-WIN-008. Højst MaxFindings fund (efter
-- tjenestenavn); resten nævnes i ét samlet fund. Inden for StartupGraceMinutes efter LastBootUpTime er reglen
-- "ikke målbar" (åbne fund forbliver åbne i stedet for at blive løst og genåbnet ved hver genstart). Viser serveren
-- slet ingen tjenester, er området "ikke målbar" (manglende læseret på Service Control Manager).
--
-- Grænseværdier (agentens appsettings.json og serverens indstillinger i SQLInsight Web):
-- SQLInsight:Servers:Services:Ignore ekstra tjenester, der må være stoppet (navn, visningsnavn eller præfiks*)
-- SQLInsight:Servers:Services:StartupGraceMinutes standard 15 (ikke målbar de første minutter efter en genstart)
-- SQLInsight:Servers:Services:MaxFindings standard 15 (1–100; fund pr. tjeneste, resten i ét samlet fund)
-- Indbygget liste over automatiske tjenester, der stopper af sig selv (StoppedAutomaticServiceRule.StopByDesign),
-- fx sppsvc, gupdate, edgeupdate, MapsBroker, RemoteRegistry, clr_optimization_*, CDPUserSvc_*.
-- Pr. server: "Tjenester der ignoreres" (insight.WindowsServers.IgnoredServices). Overvågede tjenester (SQLI-WIN-008)
-- rapporteres dér og ikke her.
--
-- Rettigheder på den overvågede server (db/permissions/grant-windows-server.ps1, aldrig lokal administrator):
-- WMI Enable Account/Remote Enable på root\cimv2 + læseret (CC LC RP RC) på Service Control Manager
-- (over netværket må ellers kun administratorer liste tjenester).
-- WS-Man: Remote Management Users · DCOM: Distributed COM Users
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/CimWindowsServerSource.cs – Session.GetServicesAsync (CimQueries.ServiceQuery, ellers
-- CimQueries.ServiceQueryWithoutDelayed), hvert 5. minut (SQLInsight:Servers:Intervals:services) fra
-- src/SQLInsight.Agent/Servers/ServerMonitor.cs – CollectServicesAsync.
-- Regelkode: src/SQLInsight.Rules/Servers/WindowsServerRules.cs : StoppedAutomaticServiceRule
--
-- Read-only: Ja. Reglen sender ingen SQL. Agenten læser kun fra Windows: CIM/WQL-forespørgsler (SELECT),
-- hændelsesloggen og for planlagte opgaver Task Schedulers skrivebeskyttede GetInfo. Den starter, stopper
-- eller ændrer intet på serveren. Filen indeholder kun kommentarer; forespørgslerne mod Windows står herunder.
-- ===================================================================================================================
-- Reglen bruger ikke SQL. Forespørgslerne mod Windows står herunder; filen indeholder kun kommentarer og ændrer intet.
--
-- CIM/WQL (namespace root\cimv2):
-- SELECT Name, DisplayName, State, StartMode, DelayedAutoStart, ExitCode FROM Win32_Service
-- SELECT Name, DisplayName, State, StartMode, ExitCode FROM Win32_Service (før Windows Server 2012)
--
-- Efterprøv med PowerShell (skrivebeskyttet; kør på agentens server som agentens konto):
-- Get-CimInstance -ComputerName SERVER -Query "SELECT Name, DisplayName, State, StartMode, DelayedAutoStart, ExitCode FROM Win32_Service" |
-- Where-Object { $_.StartMode -eq 'Auto' -and $_.State -ne 'Running' } | Select-Object Name, DisplayName, State, ExitCode

View file

@ -1,43 +0,0 @@
-- ===================================================================================================================
-- SQLI-WIN-008: En overvåget tjeneste kører ikke
-- ===================================================================================================================
-- Regel-id: SQLI-WIN-008
-- Titel: En overvåget tjeneste kører ikke
-- Alvor: High når tjenesten ikke kører; Medium når den ikke findes (eller ikke kan ses).
-- Versioner: Windows Server 2008 R2 og nyere.
--
-- Hvad tjekkes, og hvorfor:
-- Tjenester, der er valgt for serveren i SQLInsight Web (fx AOS60$01 for Dynamics AX, W3SVC for IIS, MSSQLSERVER),
-- skal køre uanset starttype. Når de ikke gør, er det, de leverer, ikke tilgængeligt. SQLInsight starter aldrig selv tjenester.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- For hvert navn på listen findes tjenesterne på navn eller visningsnavn, præcist eller med præfiks* (samme matchning
-- som SQLI-WIN-007). Findes ingen: fund (Middel). Er State ikke 'Running': fund (Høj) pr. tjeneste. Uden liste:
-- "ikke relevant". Inden for StartupGraceMinutes efter en genstart: "ikke målbar" (åbne fund forbliver åbne).
--
-- Grænseværdier (fast i koden):
-- Listen pr. server: "Overvågede tjenester" (insight.WindowsServers.WatchedServices), sat i SQLInsight Web eller API v1.
-- SQLInsight:Servers:Services:StartupGraceMinutes standard 15 (ikke målbar de første minutter efter en genstart)
--
-- Rettigheder på den overvågede server (db/permissions/grant-windows-server.ps1, aldrig lokal administrator):
-- WMI Enable Account/Remote Enable på root\cimv2 + læseret (CC LC RP RC) på Service Control Manager
-- WS-Man: Remote Management Users · DCOM: Distributed COM Users
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/CimWindowsServerSource.cs – Session.GetServicesAsync (CimQueries.ServiceQuery), hvert 5. minut fra
-- src/SQLInsight.Agent/Servers/ServerMonitor.cs – CollectServicesAsync.
-- Regelkode: src/SQLInsight.Rules/Servers/WindowsServerRules.cs : WatchedServiceRule
--
-- Read-only: Ja. Reglen sender ingen SQL. Agenten læser kun fra Windows: CIM/WQL-forespørgsler (SELECT),
-- hændelsesloggen og for planlagte opgaver Task Schedulers skrivebeskyttede GetInfo. Den starter, stopper
-- eller ændrer intet på serveren. Filen indeholder kun kommentarer; forespørgslerne mod Windows står herunder.
-- ===================================================================================================================
-- Reglen bruger ikke SQL. Forespørgslerne mod Windows står herunder; filen indeholder kun kommentarer og ændrer intet.
--
-- CIM/WQL (namespace root\cimv2):
-- SELECT Name, DisplayName, State, StartMode, DelayedAutoStart, ExitCode FROM Win32_Service
-- SELECT Name, DisplayName, State, StartMode, ExitCode FROM Win32_Service WHERE Name = '<tjeneste>' (syntetiske tjek)
--
-- Efterprøv med PowerShell (skrivebeskyttet; kør på agentens server som agentens konto):
-- Get-CimInstance -ComputerName SERVER -Query "SELECT Name, DisplayName, State, StartMode, ExitCode FROM Win32_Service WHERE Name = 'AOS60`$01'"

View file

@ -1,59 +0,0 @@
-- ===================================================================================================================
-- SQLI-WIN-009: Fejl i Windows' hændelseslog
-- ===================================================================================================================
-- Regel-id: SQLI-WIN-009
-- Titel: Fejl i Windows' hændelseslog
-- Alvor: High: disk/filsystem, ressourcer, niveau Critical, tjeneste stoppet 3+ gange. Medium: tjeneste stoppet, program gik ned,
-- Dynamics AX. Low: almindelige fejl der gentager sig. (Uventet nedlukning: se SQLI-WIN-010.)
-- Versioner: Windows Server 2008 R2 og nyere.
--
-- Hvad tjekkes, og hvorfor:
-- Fejl og kritiske hændelser i System- og Application-loggen: disk- og filsystemfejl, opbrugte ressourcer, tjenester
-- der stopper eller ikke starter, programmer der går ned, fejl fra Dynamics AX' AOS – og almindelige fejl, der gentager sig.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- Hændelser med niveau 1 (kritisk) og 2 (fejl) siden sidste indsamling læses fra System og Application. Støj tælles,
-- men grupperes ikke. Resten grupperes pr. log, kilde og hændelses-id med antal, første og seneste tidspunkt og den
-- nyeste tekst som eksempel (følsom: vises kun lokalt, [udeladt] i API'et). Antallet holdes pr. klokketime, og kun de
-- timer, der har hændelser inden for de seneste 24 timer, lægges sammen – en fejl, der kommer hver 10. time, tæller
-- altså aldrig mere end de 2–3 gange, den faktisk er set på et døgn. Vigtige kategorier giver fund med det samme,
-- almindelige fejl først ved RepeatThreshold gentagelser. Højst MaxFindings fund i en fast rækkefølge (kategori, så den
-- ældste først, så nøglen), så de samme hændelser beholder deres fund; resten nævnes i ét samlet fund med den højeste
-- alvor blandt dem.
--
-- Grænseværdier (agentens appsettings.json):
-- SQLInsight:Servers:EventLog:RepeatThreshold standard 5 (en almindelig fejl skal ses så mange gange på 24 timer)
-- SQLInsight:Servers:EventLog:Ignore ekstra støj: "Kilde:id", "Kilde" eller "Kilde*"
-- SQLInsight:Servers:EventLog:MaxFindings standard 15 (1–100; fund pr. hændelse, vigtigste først, resten i ét samlet fund)
-- SQLInsight:Servers:EventLog:MaxEventsPerCollection standard 2000 (hændelser pr. log pr. indsamling)
-- Indbygget støjliste (ServerEventAnalysis.DefaultIgnore): DCOM 10016/10010, Schannel 36874/36887/36888, TPM-WMI 1796/1801,
-- WMI 10, Security-SPP 8198, og SQL Server's egne kilder (MSSQLSERVER, MSSQL$*, SQLSERVERAGENT, SQLAgent$*,
-- SQLISPackage*), som SQL Server-fejlloggen allerede dækker. Et fund holdes åbent 24 timer efter hændelsen sidst blev set.
--
-- Rettigheder på den overvågede server (db/permissions/grant-windows-server.ps1, aldrig lokal administrator):
-- Event Log Readers (System- og Application-loggen; aldrig Security-loggen).
-- Over RPC (MS-EVEN6): firewallreglerne "Remote Event Log Management". Fallback over CIM: WMI Remote Enable på root\cimv2.
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/CimWindowsServerSource.cs – Session.GetEventsAsync: EventLogReader med CimQueries.EventXPath,
-- ellers CimQueries.EventQuery; hvert 5. minut (SQLInsight:Servers:Intervals:events) fra
-- src/SQLInsight.Agent/Servers/ServerMonitor.cs – CollectEventsAsync. Gruppering og støjfilter:
-- src/SQLInsight.Core/Servers/ServerEvents.cs – ServerEventAnalysis.Group/Recent/Categorize.
-- Regelkode: src/SQLInsight.Rules/Servers/WindowsServerRules.cs : WindowsEventLogRule
--
-- Read-only: Ja. Reglen sender ingen SQL. Agenten læser kun fra Windows: CIM/WQL-forespørgsler (SELECT),
-- hændelsesloggen og for planlagte opgaver Task Schedulers skrivebeskyttede GetInfo. Den starter, stopper
-- eller ændrer intet på serveren. Filen indeholder kun kommentarer; forespørgslerne mod Windows står herunder.
-- ===================================================================================================================
-- Reglen bruger ikke SQL. Forespørgslerne mod Windows står herunder; filen indeholder kun kommentarer og ændrer intet.
--
-- Event Log (MS-EVEN6), XPath pr. log ('System' og 'Application'):
-- *[System[(Level=1 or Level=2) and TimeCreated[@SystemTime>'2026-10-02T12:00:00.000Z']]]
-- CIM/WQL fallback (namespace root\cimv2; EventType 1 = fejl, kritiske hændelser rapporteres som fejl):
-- SELECT Logfile, SourceName, EventCode, EventType, TimeGenerated, RecordNumber, Message FROM Win32_NTLogEvent
-- WHERE Logfile = 'System' AND EventType = 1 AND TimeGenerated > '20261002120000.000000+000'
--
-- Efterprøv med PowerShell (skrivebeskyttet; kør på agentens server som agentens konto):
-- Get-WinEvent -ComputerName SERVER -LogName System -FilterXPath "*[System[(Level=1 or Level=2) and TimeCreated[timediff(@SystemTime) <= 86400000]]]" |
-- Group-Object ProviderName, Id | Sort-Object Count -Descending | Select-Object Count, Name

View file

@ -1,45 +0,0 @@
-- ===================================================================================================================
-- SQLI-WIN-010: Serveren er genstartet
-- ===================================================================================================================
-- Regel-id: SQLI-WIN-010
-- Titel: Serveren er genstartet
-- Alvor: High ved uventet genstart; Info ved genstart uden tegn på at den var uventet; Low når hændelsesloggen ikke kan læses.
-- Versioner: Windows Server 2008 R2 og nyere.
--
-- Hvad tjekkes, og hvorfor:
-- Serveren er genstartet det seneste døgn. Uventet (strøm, blå skærm, hård nulstilling) afbryder alt midt i det, og
-- databaser og AX skal gendanne sig selv; en planlagt genstart (fx Windows-opdateringer) er kun til orientering.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- Er LastBootUpTime inden for 24 timer, er serveren genstartet. Er der i hændelsesloggen (seneste 24 timer) en hændelse i
-- kategorien "uventet nedlukning" – Microsoft-Windows-Kernel-Power 41, EventLog 6008, BugCheck/WER-SystemErrorReporting 1001 –
-- senest set tidligst en time før starttidspunktet, er genstarten uventet (Høj), ellers Info. "Planlagt" siges kun, når
-- hændelsesloggen er læst efter starttidspunktet: er den læst sidst før genstarten, er reglen "ikke målbar", indtil den
-- er indsamlet igen; kan den slet ikke læses, er fundet Low og siger, at årsagen ikke kan ses.
--
-- Grænseværdier (fast i koden):
-- Fast: 24 timer efter LastBootUpTime (ServerEventAnalysis.RecentWindow), derefter forsvinder fundet af sig selv.
--
-- Rettigheder på den overvågede server (db/permissions/grant-windows-server.ps1, aldrig lokal administrator):
-- WMI Enable Account/Remote Enable på root\cimv2 (starttidspunkt); Event Log Readers (hvorfor).
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/CimWindowsServerSource.cs – Session.GetOperatingSystemAsync (CimQueries.OperatingSystemQuery) og
-- Session.GetEventsAsync, fra src/SQLInsight.Agent/Servers/ServerMonitor.cs – CollectPerformanceAsync og CollectEventsAsync.
-- Regelkode: src/SQLInsight.Rules/Servers/WindowsServerRules.cs : ServerRestartRule
--
-- Read-only: Ja. Reglen sender ingen SQL. Agenten læser kun fra Windows: CIM/WQL-forespørgsler (SELECT),
-- hændelsesloggen og for planlagte opgaver Task Schedulers skrivebeskyttede GetInfo. Den starter, stopper
-- eller ændrer intet på serveren. Filen indeholder kun kommentarer; forespørgslerne mod Windows står herunder.
-- ===================================================================================================================
-- Reglen bruger ikke SQL. Forespørgslerne mod Windows står herunder; filen indeholder kun kommentarer og ændrer intet.
--
-- CIM/WQL (namespace root\cimv2):
-- SELECT Caption, Version, BuildNumber, LastBootUpTime, TotalVisibleMemorySize, CSName FROM Win32_OperatingSystem
-- Event Log (MS-EVEN6), XPath for System:
-- *[System[(Level=1 or Level=2) and TimeCreated[@SystemTime>'2026-10-02T12:00:00.000Z']]]
--
-- Efterprøv med PowerShell (skrivebeskyttet; kør på agentens server som agentens konto):
-- (Get-CimInstance -ComputerName SERVER -Query "SELECT LastBootUpTime FROM Win32_OperatingSystem").LastBootUpTime
-- Get-WinEvent -ComputerName SERVER -FilterHashtable @{ LogName = 'System'; Id = 41, 6008, 1001, 1074; StartTime = (Get-Date).AddDays(-1) }

View file

@ -1,40 +0,0 @@
-- ===================================================================================================================
-- SQLI-WIN-011: Serveren er ikke genstartet i lang tid
-- ===================================================================================================================
-- Regel-id: SQLI-WIN-011
-- Titel: Serveren er ikke genstartet i lang tid
-- Alvor: Low.
-- Versioner: Windows Server 2008 R2 og nyere.
--
-- Hvad tjekkes, og hvorfor:
-- De fleste Windows-opdateringer kræver en genstart. En server, der ikke er genstartet i flere måneder, mangler derfor
-- næsten sikkert sikkerhedsopdateringer.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- Oppetid = nu − LastBootUpTime. Er UptimeDays større end 0, og oppetiden mindst så mange dage, er det et fund.
-- Med UptimeDays = 0 er reglen "ikke relevant".
--
-- Grænseværdier (agentens appsettings.json):
-- SQLInsight:Servers:UptimeDays standard 0 = slået fra (fx 120 for et fund efter 120 dage uden genstart)
--
-- Rettigheder på den overvågede server (db/permissions/grant-windows-server.ps1, aldrig lokal administrator):
-- WMI Enable Account/Remote Enable på root\cimv2
-- WS-Man: Remote Management Users · DCOM: Distributed COM Users
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/CimWindowsServerSource.cs – Session.GetOperatingSystemAsync (CimQueries.OperatingSystemQuery),
-- fra src/SQLInsight.Agent/Servers/ServerMonitor.cs – CollectPerformanceAsync.
-- Regelkode: src/SQLInsight.Rules/Servers/WindowsServerRules.cs : ServerUptimeRule
--
-- Read-only: Ja. Reglen sender ingen SQL. Agenten læser kun fra Windows: CIM/WQL-forespørgsler (SELECT),
-- hændelsesloggen og for planlagte opgaver Task Schedulers skrivebeskyttede GetInfo. Den starter, stopper
-- eller ændrer intet på serveren. Filen indeholder kun kommentarer; forespørgslerne mod Windows står herunder.
-- ===================================================================================================================
-- Reglen bruger ikke SQL. Forespørgslen mod Windows står herunder; filen indeholder kun kommentarer og ændrer intet.
--
-- CIM/WQL (namespace root\cimv2):
-- SELECT Caption, Version, BuildNumber, LastBootUpTime, TotalVisibleMemorySize, CSName FROM Win32_OperatingSystem
--
-- Efterprøv med PowerShell (skrivebeskyttet; kør på agentens server som agentens konto):
-- (Get-Date) - (Get-CimInstance -ComputerName SERVER -Query "SELECT LastBootUpTime FROM Win32_OperatingSystem").LastBootUpTime

View file

@ -1,55 +0,0 @@
-- ===================================================================================================================
-- SQLI-WIN-012: En planlagt opgave fejlede sidste gang, den kørte
-- ===================================================================================================================
-- Regel-id: SQLI-WIN-012
-- Titel: En planlagt opgave fejlede sidste gang, den kørte
-- Alvor: Medium.
-- Versioner: Windows Server 2012 og nyere (navnerummet TaskScheduler findes ikke på 2008 R2: "ikke målbar").
--
-- Hvad tjekkes, og hvorfor:
-- En opgave i Opgavestyring (Task Scheduler) sluttede med en fejlkode. Planlagte opgaver er ofte oprydning, eksport,
-- integrationer eller filbackup: når de fejler, opdager ingen det, før noget mangler.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- Et fund pr. aktiveret, overvåget opgave, hvis LastTaskResult er en fejl. Indeholder opgavens navn en brugers SID
-- (S-1-5-21-…), står den i titlen som "bruger-" og en kort hash; det fulde navn kun som følsom måling.
--
-- Grænseværdier (agentens appsettings.json):
-- SQLInsight:Servers:Tasks:IncludeMicrosoftTasks standard false (Microsofts egne opgaver under \Microsoft\ overvåges ikke)
-- SQLInsight:Servers:Tasks:IgnoreUserAndUpdaterTasks standard true (opgaver pr. bruger – en SID i navnet – og programmers
-- egne opdateringsopgaver: OneDrive, MicrosoftEdgeUpdate, GoogleUpdate,
-- User_Feed_Synchronization; TaskResults.DefaultIgnoredNames)
-- SQLInsight:Servers:Tasks:Ignore opgaver der aldrig overvåges (sti\navn eller præfiks*)
-- SQLInsight:Servers:Tasks:MaxWatched standard 50 (højst så mange opgaver pr. server)
-- Resultater, der ikke er fejl: 0x0, 0x41300 (klar), 0x41301 (kører), 0x41302 (deaktiveret), 0x41303 (har ikke kørt),
-- 0x41304 (ingen flere kørsler), 0x41325 (i kø), 0x8004131F (kørte allerede) – src/SQLInsight.Core/Servers/ServerTasks.cs.
--
-- Rettigheder på den overvågede server (db/permissions/grant-windows-server.ps1, aldrig lokal administrator):
-- WMI Enable Account/Remote Enable på root\Microsoft\Windows\TaskScheduler. Agenten ser kun opgaver, hvis egne
-- sikkerhedsindstillinger tillader kontoen at læse dem. Seneste resultat læses med Task Schedulers GetInfo (et
-- skrivebeskyttet CIM-metodekald, som Get-ScheduledTaskInfo); over WinRM kræver metodekald, at kontoen også har
-- Execute-ret i WinRM – ellers bruges DCOM.
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/CimWindowsServerSource.cs – Session.GetTasksAsync (CimQueries.TaskQuery og
-- PS_ScheduledTask.GetInfoByName), hvert 15. minut (SQLInsight:Servers:Intervals:tasks) fra
-- src/SQLInsight.Agent/Servers/ServerMonitor.cs – CollectTasksAsync.
-- Regelkode: src/SQLInsight.Rules/Servers/WindowsServerRules.cs : FailedScheduledTaskRule
--
-- Read-only: Ja. Reglen sender ingen SQL. Agenten læser kun fra Windows: CIM/WQL-forespørgsler (SELECT),
-- hændelsesloggen og for planlagte opgaver Task Schedulers skrivebeskyttede GetInfo. Den starter, stopper
-- eller ændrer intet på serveren. Filen indeholder kun kommentarer; forespørgslerne mod Windows står herunder.
-- ===================================================================================================================
-- Reglen bruger ikke SQL. Forespørgslerne mod Windows står herunder; filen indeholder kun kommentarer og ændrer intet.
--
-- CIM/WQL (namespace root\Microsoft\Windows\TaskScheduler):
-- SELECT TaskName, TaskPath, State FROM MSFT_ScheduledTask
-- CIM-metode (skrivebeskyttet) pr. overvåget opgave:
-- PS_ScheduledTask.GetInfoByName(TaskName, TaskPath) -> cmdletOutput: MSFT_TaskDynamicInfo
-- (LastRunTime, LastTaskResult, NextRunTime, NumberOfMissedRuns)
--
-- Efterprøv med PowerShell (skrivebeskyttet; kør på agentens server som agentens konto):
-- $s = New-CimSession -ComputerName SERVER
-- Get-ScheduledTask -CimSession $s | Where-Object TaskPath -notlike '\Microsoft\*' | Get-ScheduledTaskInfo -CimSession $s |
-- Select-Object TaskPath, TaskName, LastRunTime, @{n='Resultat';e={'0x{0:X}' -f $_.LastTaskResult}}, NextRunTime, NumberOfMissedRuns

View file

@ -1,47 +0,0 @@
-- ===================================================================================================================
-- SQLI-WIN-013: En planlagt opgave kørte ikke, da den skulle
-- ===================================================================================================================
-- Regel-id: SQLI-WIN-013
-- Titel: En planlagt opgave kørte ikke, da den skulle
-- Alvor: Medium.
-- Versioner: Windows Server 2012 og nyere.
--
-- Hvad tjekkes, og hvorfor:
-- Opgaven skulle have kørt, men gjorde det ikke: Opgavestyring kørte ikke, betingelserne var ikke opfyldt (fx "kun når
-- brugeren er logget på"), eller serveren var slukket. Det, opgaven gør, er ikke sket.
--
-- Vurdering (sker i C# ud fra de indsamlede data, ikke på den overvågede server):
-- Et fund pr. aktiveret, overvåget opgave, der ikke kører nu, og hvor enten NumberOfMissedRuns > 0, eller NextRunTime
-- ligger mere end LateMinutes i fortiden, uden at LastRunTime er efter det tidspunkt.
--
-- Grænseværdier (agentens appsettings.json):
-- SQLInsight:Servers:Tasks:LateMinutes standard 60 (næste kørsel mere end så mange minutter i fortiden)
-- SQLInsight:Servers:Tasks:IncludeMicrosoftTasks standard false
-- SQLInsight:Servers:Tasks:IgnoreUserAndUpdaterTasks standard true (opgaver pr. bruger og opdateringsopgaver, se SQLI-WIN-012)
-- SQLInsight:Servers:Tasks:Ignore opgaver der aldrig overvåges
-- En brugers SID i opgavens navn står aldrig i titlen ("bruger-" og en kort hash), kun i den følsomme måling.
--
-- Rettigheder på den overvågede server (db/permissions/grant-windows-server.ps1, aldrig lokal administrator):
-- WMI Enable Account/Remote Enable på root\Microsoft\Windows\TaskScheduler (se SQLI-WIN-012 om GetInfo over WinRM).
--
-- Data kommer fra (collector, fil – medlemmer, interval):
-- src/SQLInsight.Servers/CimWindowsServerSource.cs – Session.GetTasksAsync (CimQueries.TaskQuery og
-- PS_ScheduledTask.GetInfoByName), hvert 15. minut fra src/SQLInsight.Agent/Servers/ServerMonitor.cs – CollectTasksAsync.
-- Regelkode: src/SQLInsight.Rules/Servers/WindowsServerRules.cs : MissedScheduledTaskRule
--
-- Read-only: Ja. Reglen sender ingen SQL. Agenten læser kun fra Windows: CIM/WQL-forespørgsler (SELECT),
-- hændelsesloggen og for planlagte opgaver Task Schedulers skrivebeskyttede GetInfo. Den starter, stopper
-- eller ændrer intet på serveren. Filen indeholder kun kommentarer; forespørgslerne mod Windows står herunder.
-- ===================================================================================================================
-- Reglen bruger ikke SQL. Forespørgslerne mod Windows står herunder; filen indeholder kun kommentarer og ændrer intet.
--
-- CIM/WQL (namespace root\Microsoft\Windows\TaskScheduler):
-- SELECT TaskName, TaskPath, State FROM MSFT_ScheduledTask
-- CIM-metode (skrivebeskyttet) pr. overvåget opgave:
-- PS_ScheduledTask.GetInfoByName(TaskName, TaskPath) -> cmdletOutput: MSFT_TaskDynamicInfo
--
-- Efterprøv med PowerShell (skrivebeskyttet; kør på agentens server som agentens konto):
-- $s = New-CimSession -ComputerName SERVER
-- Get-ScheduledTask -CimSession $s | Where-Object { $_.TaskPath -notlike '\Microsoft\*' -and $_.State -ne 'Disabled' } |
-- Get-ScheduledTaskInfo -CimSession $s | Where-Object { $_.NumberOfMissedRuns -gt 0 -or ($_.NextRunTime -and $_.NextRunTime -lt (Get-Date).AddMinutes(-60)) }

View file

@ -1,98 +0,0 @@
-- ===================================================================================================================
-- Collector: AxBatchCollector ("dynamics-ax-batch")
-- ===================================================================================================================
-- Hvad: Hvert 5. minut, i hver genkendt og overvåget AX-database: batchjob (BATCH i Axapta 3.0/4.0,
-- BATCHJOB fra AX 2009) og - fra AX 2009 - de afsluttede kørsler i BATCHJOBHISTORY efter seneste læste
-- RECID (minus et overlap, fordi AOS'er får RecId i blokke). Axapta 3.0/4.0 har ingen historiktabel; der
-- gemmer SQLInsight de kørsler, den selv ser blive afsluttet. Kørslerne gemmes i SQLInsights repository
-- (insight.AxBatchRuns) - aldrig i AX.
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxBatchCollector.cs; SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs
-- Interval: 5 min som standard (SQLInsight:CollectorIntervals:dynamics-ax-batch)
-- Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema (docs/dynamics-ax/
-- schema-ax30.tsv). AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Rettigheder til agentens login (se db/permissions/grant-least-privilege.sql og grant-dynamics-ax-read.sql):
-- Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql; [bruger] = agentens databasebruger):
-- Axapta 3.0: GRANT SELECT ON [bmssa].[BATCH] TO [bruger];
-- AX 4.0: GRANT SELECT ON [dbo].[BATCH] TO [bruger];
-- AX 2009/2012/F&O: GRANT SELECT ON [dbo].[BATCHJOB] TO [bruger];
-- GRANT SELECT ON [dbo].[BATCHJOBHISTORY] TO [bruger];
-- Mangler den, er reglen "ikke målbar" med den nøjagtige GRANT for databasen (også under Dynamics AX → AX-databaser
-- og GET /api/v1/dynamics-ax/grant-script). Uden GRANT ændres intet.
-- Genkendelsen: kun grant-least-privilege.sql (VIEW ANY DEFINITION; CONNECT ANY DATABASE fra 2014, ellers en
-- databasebruger: CREATE USER [agent] FOR LOGIN [agent];).
--
-- Bruges af: SQLI-AX-001, SQLI-AX-002, SQLI-AX-003, SQLI-AX-004 og Dynamics AX → Batchjob
-- Read-only: Ja: kun SELECT med TOP og SET-sessionsindstillinger. AX-tabellerne læses i AX-databasen via
-- [db].sys.sp_executesql (sessionen skifter aldrig database) under READ COMMITTED med SET LOCK_TIMEOUT 2000
-- (SNAPSHOT, når databasen tillader det), aldrig NOLOCK; bagefter sættes agentens sessionsindstillinger
-- igen. Intet ændrer data, indstillinger eller objekter. Kun i databaser genkendt som AX og valgt til
-- overvågning.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); varianter for andre AX-versioner end din fejler eller giver ingen rækker - det er forventet.
-- AX-forespørgslerne er vist for databasen AXDB: ret [AXDB] til din AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
-- AxBatchCollector: Batchjob og kørselshistorik i AX-databasen (valgfri AX-læseadgang)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Axapta 3.0 (skema bmssa, verificeret mod kundedata): batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(3.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[BATCH] b ORDER BY b.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 4.0 (skema dbo) - ikke verificeret mod kundedata: batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(4.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCH] b ORDER BY b.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(2009)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (de nyeste)
-- Kilde: AxSchemaAdapter.BatchHistory(2009, første)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId: null) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger (fra den højeste RECID, SQLInsight har læst, minus overlappet)
-- Kilde: AxSchemaAdapter.BatchHistory(2009, næste)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId) via AxSql.InDatabase
-- Agenten indsætter den højeste RECID, den har læst, minus overlappet, hvor der her står 0.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (3000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 0 ORDER BY h.[RECID]';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob
-- Kilde: AxSchemaAdapter.BatchJobs(2012)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchJobs(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (de nyeste)
-- Kilde: AxSchemaAdapter.BatchHistory(2012, første)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId: null) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger (fra den højeste RECID, SQLInsight har læst, minus overlappet)
-- Kilde: AxSchemaAdapter.BatchHistory(2012, næste)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.BatchHistory(database, afterRecId) via AxSql.InDatabase
-- Agenten indsætter den højeste RECID, den har læst, minus overlappet, hvor der her står 0.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (3000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 0 ORDER BY h.[RECID]';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO

View file

@ -1,89 +0,0 @@
-- ===================================================================================================================
-- Collector: AxDatabaseDetector ("dynamics-ax", og før hver AX-collector og AX-community-regel)
-- ===================================================================================================================
-- Hvad: Finder de overvågede databaser, der er Dynamics AX: SQLDICTIONARY og DATAAREA i samme skema.
-- Versionen afgøres af skemaet (src/SQLInsight.Core/DynamicsAx/AxDetection.cs : Recognise): RECID int
-- -> Axapta 2.5/3.0 (typisk skema bmssa); bigint uden BATCHJOB -> AX 4.0; BATCHJOB uden
-- NUMBERSEQUENCESCOPE -> AX 2009; NUMBERSEQUENCESCOPE uden PARTITION -> AX 2012 RTM/FP; PARTITION og
-- <db>_model -> AX 2012 R2/R3; PARTITION uden modellager -> Dynamics 365 F&O (formodet). Læser kun
-- metadata (sys.tables, sys.columns, HAS_PERMS_BY_NAME) og - hvis agenten må - firmalisten i DATAAREA.
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs (collector: src/SQLInsight.Collectors/DynamicsAx/DynamicsAxCollector.cs)
-- Interval: 1 time som standard (SQLInsight:CollectorIntervals:dynamics-ax); en AX-database uden AX-læseadgang hvert 5. minut
-- Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema (docs/dynamics-ax/
-- schema-ax30.tsv). AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Rettigheder til agentens login (se db/permissions/grant-least-privilege.sql og grant-dynamics-ax-read.sql):
-- Genkendelsen: kun grant-least-privilege.sql (VIEW ANY DEFINITION; CONNECT ANY DATABASE fra 2014, ellers en
-- databasebruger). Firmalisten: valgfri AX-læseadgang, GRANT SELECT ON [bmssa].[DATAAREA] TO [bruger];
-- ([dbo] efter Axapta 3.0).
--
-- Bruges af: SQLI-AX-001..010, COM-AX-001..007, AxBatchCollector, AxSequenceCollector og siderne under Dynamics AX
-- Read-only: Ja: kun SELECT med TOP og SET-sessionsindstillinger. AX-tabellerne læses i AX-databasen via
-- [db].sys.sp_executesql (sessionen skifter aldrig database) under READ COMMITTED med SET LOCK_TIMEOUT 2000
-- (SNAPSHOT, når databasen tillader det), aldrig NOLOCK; bagefter sættes agentens sessionsindstillinger
-- igen. Intet ændrer data, indstillinger eller objekter. Kun i databaser genkendt som AX og valgt til
-- overvågning.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); varianter for andre AX-versioner end din fejler eller giver ingen rækker - det er forventet.
-- AX-forespørgslerne er vist for databasen AXDB: ret [AXDB] til din AX-database.
-- ===================================================================================================================
-- Variant: Alle versioner fra SQL Server 2005: online brugerdatabaser og deres snapshot-indstillinger
-- Kilde: AxDatabaseDetector.DatabasesSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.DatabasesSql
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
GO
-- Variant: Agentens login, til GRANT-teksten, når AX-læseadgangen mangler
-- Kilde: AxDatabaseDetector.LoginSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.LoginSql
SELECT SUSER_SNAME()
GO
-- Variant: SQL Server 2005 og nyere: AX-genkendelsen i hver overvåget database (kun metadata; vist for AXDB)
-- Kilde: AxDatabaseDetector.BuildDetectionSql
-- Kode: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs : AxDatabaseDetector.BuildDetectionSql(database) = AxSql.InDatabase(database, AxDetection.MetadataSql)
-- Agenten kører dette i hver overvåget database fra listen ovenfor (ikke *_model). Ret [AXDB] til den database, du vil tjekke.
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: Axapta 3.0 (skema bmssa, verificeret mod kundedata): firmaerne (DATAAREA), kun med AX-læseadgang
-- Kilde: AxSchemaAdapter.Companies(3.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.Companies(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) d.[ID], d.[NAME], d.[ISVIRTUAL] FROM [bmssa].[DATAAREA] d ORDER BY d.[ID]';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 4.0 (skema dbo) - ikke verificeret mod kundedata: firmaerne (DATAAREA), kun med AX-læseadgang
-- Kilde: AxSchemaAdapter.Companies(4.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.Companies(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) d.[ID], d.[NAME], d.[ISVIRTUAL] FROM [dbo].[DATAAREA] d ORDER BY d.[ID]';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: firmaerne (DATAAREA), kun med AX-læseadgang
-- Kilde: AxSchemaAdapter.Companies(2009)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.Companies(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) d.[ID], d.[NAME], d.[ISVIRTUAL] FROM [dbo].[DATAAREA] d ORDER BY d.[ID]';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: firmaerne (DATAAREA), kun med AX-læseadgang
-- Kilde: AxSchemaAdapter.Companies(2012)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.Companies(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) d.[ID], d.[NAME], d.[ISVIRTUAL] FROM [dbo].[DATAAREA] d ORDER BY d.[ID]';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO

View file

@ -1,105 +0,0 @@
-- ===================================================================================================================
-- Collector: AxSequenceCollector ("dynamics-ax-sequences")
-- ===================================================================================================================
-- Hvad: Hver time, i hver genkendt og overvåget AX-database: de mest brugte nummerserier
-- (NUMBERSEQUENCETABLE, med firma og om en reference bruger serien) og de højeste RecId-serier
-- (SYSTEMSEQUENCES). SQLInsight gemmer én måling pr. serie pr. dag i sit eget repository
-- (insight.AxSequenceSamples) og laver prognoserne ud fra den - aldrig i AX.
-- Fil: src/SQLInsight.Collectors/DynamicsAx/AxSequenceCollector.cs; SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs
-- Interval: 1 time som standard (SQLInsight:CollectorIntervals:dynamics-ax-sequences)
-- Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema (docs/dynamics-ax/
-- schema-ax30.tsv). AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema - ikke
-- verificeret mod kundedata.
--
-- Rettigheder til agentens login (se db/permissions/grant-least-privilege.sql og grant-dynamics-ax-read.sql):
-- Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql; [bruger] = agentens databasebruger):
-- Axapta 3.0: GRANT SELECT ON [bmssa].[NUMBERSEQUENCETABLE] TO [bruger];
-- GRANT SELECT ON [bmssa].[NUMBERSEQUENCEREFERENCE] TO [bruger]; -- valgfri: om serien bruges
-- AX 4.0/2009: de samme tabeller i [dbo]
-- AX 2012/F&O: desuden GRANT SELECT ON [dbo].[NUMBERSEQUENCESCOPE] TO [bruger]; -- valgfri: firma
-- Mangler NUMBERSEQUENCETABLE, er reglen "ikke målbar" med den nøjagtige GRANT for databasen. Uden GRANT ændres intet.
-- Genkendelsen: kun grant-least-privilege.sql (VIEW ANY DEFINITION; CONNECT ANY DATABASE fra 2014, ellers en
-- databasebruger: CREATE USER [agent] FOR LOGIN [agent];).
-- Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql; [bruger] = agentens databasebruger):
-- Axapta 3.0: GRANT SELECT ON [bmssa].[SYSTEMSEQUENCES] TO [bruger];
-- GRANT SELECT ON [bmssa].[SQLDICTIONARY] TO [bruger]; -- valgfri: tabelnavne
-- AX 4.0/2009/2012/F&O: de samme tabeller i [dbo]
-- Mangler SYSTEMSEQUENCES, er reglen "ikke målbar" med den nøjagtige GRANT for databasen. Uden GRANT ændres intet.
-- Genkendelsen: kun grant-least-privilege.sql (VIEW ANY DEFINITION; CONNECT ANY DATABASE fra 2014, ellers en
-- databasebruger: CREATE USER [agent] FOR LOGIN [agent];).
--
-- Bruges af: SQLI-AX-005..009 og Dynamics AX → Nummerserier og RecId
-- Read-only: Ja: kun SELECT med TOP og SET-sessionsindstillinger. AX-tabellerne læses i AX-databasen via
-- [db].sys.sp_executesql (sessionen skifter aldrig database) under READ COMMITTED med SET LOCK_TIMEOUT 2000
-- (SNAPSHOT, når databasen tillader det), aldrig NOLOCK; bagefter sættes agentens sessionsindstillinger
-- igen. Intet ændrer data, indstillinger eller objekter. Kun i databaser genkendt som AX og valgt til
-- overvågning.
-- Kørsel: Kør i SSMS som agentens login (eller et login med samme rettigheder). Hver variant er sin egen batch
-- (GO); varianter for andre AX-versioner end din fejler eller giver ingen rækker - det er forventet.
-- AX-forespørgslerne er vist for databasen AXDB: ret [AXDB] til din AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
-- AxSequenceCollector: Nummerserier i AX-databasen (valgfri AX-læseadgang)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Axapta 3.0 (skema bmssa, verificeret mod kundedata): nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(3.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [bmssa].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 4.0 (skema dbo) - ikke verificeret mod kundedata: nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(4.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(2009)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier
-- Kilde: AxSchemaAdapter.NumberSequences(2012)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.NumberSequences(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], NULLIF(sc.[DATAAREA], N''''), n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCEID] = n.[RECID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n LEFT JOIN [dbo].[NUMBERSEQUENCESCOPE] sc ON sc.[RECID] = n.[NUMBERSEQUENCESCOPE] ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- -------------------------------------------------------------------------------------------------------------------
-- AxSequenceCollector: RecId pr. tabel og firma i AX-databasen (valgfri AX-læseadgang)
-- -------------------------------------------------------------------------------------------------------------------
-- Variant: Axapta 3.0 (skema bmssa, verificeret mod kundedata): RecId pr. tabel (SYSTEMSEQUENCES)
-- Kilde: AxSchemaAdapter.RecIds(3.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.RecIds(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [bmssa].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [bmssa].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 4.0 (skema dbo) - ikke verificeret mod kundedata: RecId pr. tabel (SYSTEMSEQUENCES)
-- Kilde: AxSchemaAdapter.RecIds(4.0)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.RecIds(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [dbo].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [dbo].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2009 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: RecId pr. tabel (SYSTEMSEQUENCES)
-- Kilde: AxSchemaAdapter.RecIds(2009)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.RecIds(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [dbo].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [dbo].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: AX 2012 RTM/R2/R3 - baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: RecId pr. tabel (SYSTEMSEQUENCES)
-- Kilde: AxSchemaAdapter.RecIds(2012)
-- Kode: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs : AxSchemaAdapter.RecIds(database) via AxSql.InDatabase
EXEC [AXDB].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [dbo].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [dbo].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO

View file

@ -1,8 +1,7 @@
-- ===================================================================================================================
-- Collector: BlockingCollector ("blocking")
-- ===================================================================================================================
-- Hvad: Blokerede sessioner og deres blokerere lige nu, med sætning, åbne transaktioner og CONTEXT_INFO (fra 1.4:
-- Dynamics AX 2009/2012 AOS kan lægge AX-brugeren dér).
-- Hvad: Blokerede sessioner og deres blokerere lige nu, med sætning og åbne transaktioner.
-- Fil: src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs
-- Interval: 1 min som standard (SQLInsight:CollectorIntervals:blocking)
-- Versioner: Alle (SQL Server 2000 og nyere)
@ -13,7 +12,7 @@
-- GRANT VIEW SERVER SECURITY STATE TO [agent]; -- SQL Server 2022+
-- SQL Server 2000 har ikke VIEW SERVER STATE; se docs/legacy-versions.md (fx kræver DBCC SQLPERF(WAITSTATS) forhøjede rettigheder).
--
-- Bruges af: SQLI-BLK-001, SQLI-AX-010
-- Bruges af: SQLI-BLK-001
-- Read-only: Ja: kun SELECT samt SET-sessionsindstillinger. Intet ændrer data, indstillinger eller objekter.
-- Kørsel: Kør i SSMS som agentens login. Hver variant er sin egen batch (GO); en variant for en anden SQL Server-version
-- end din kan fejle - det er forventet. Agenten kører altid med sessionsindstillingerne i _collectors/InstanceProbe.sql.
@ -29,7 +28,7 @@ SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.stat
(CASE WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END
- r.statement_start_offset) / 2 + 1)
ELSE t.text END, 4000),
s.last_request_end_time, s.context_info
s.last_request_end_time
FROM sys.dm_exec_sessions s
LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id
LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.session_id
@ -38,26 +37,7 @@ WHERE r.blocking_session_id > 0
OR s.session_id IN (SELECT blocking_session_id FROM sys.dm_exec_requests WHERE blocking_session_id > 0)
GO
-- Variant: SQL Server 2008 – 2008 R2
-- Kilde: BlockingCollector.BuildSql2005(2008)
-- Kode: src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs : BlockingCollector.BuildSql2005(version)
SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.status), r.wait_type, r.wait_time, r.wait_resource,
DB_NAME(r.database_id), s.login_name, s.host_name, s.program_name, (SELECT COUNT(*) FROM sys.dm_tran_session_transactions st WHERE st.session_id = s.session_id),
LEFT(CASE WHEN r.sql_handle IS NOT NULL AND r.statement_start_offset IS NOT NULL
THEN SUBSTRING(t.text, r.statement_start_offset / 2 + 1,
(CASE WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END
- r.statement_start_offset) / 2 + 1)
ELSE t.text END, 4000),
s.last_request_end_time, s.context_info
FROM sys.dm_exec_sessions s
LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id
LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.session_id
OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, c.most_recent_sql_handle)) t
WHERE r.blocking_session_id > 0
OR s.session_id IN (SELECT blocking_session_id FROM sys.dm_exec_requests WHERE blocking_session_id > 0)
GO
-- Variant: SQL Server 2005 (CONTEXT_INFO fra sys.sysprocesses)
-- Variant: SQL Server 2005 – 2008 R2
-- Kilde: BlockingCollector.BuildSql2005(2005)
-- Kode: src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs : BlockingCollector.BuildSql2005(version)
SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.status), r.wait_type, r.wait_time, r.wait_resource,
@ -67,7 +47,7 @@ SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.stat
(CASE WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END
- r.statement_start_offset) / 2 + 1)
ELSE t.text END, 4000),
s.last_request_end_time, (SELECT TOP 1 p.context_info FROM sys.sysprocesses p WHERE p.spid = s.session_id AND p.ecid = 0)
s.last_request_end_time
FROM sys.dm_exec_sessions s
LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id
LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.session_id
@ -81,7 +61,7 @@ GO
-- Kode: src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs : BlockingCollector.Sql2000
SELECT p.spid, NULLIF(CASE WHEN p.blocked = p.spid THEN 0 ELSE p.blocked END, 0), RTRIM(p.status), RTRIM(p.lastwaittype),
p.waittime, RTRIM(p.waitresource), DB_NAME(p.dbid), RTRIM(p.loginame), RTRIM(p.hostname), RTRIM(p.program_name),
p.open_tran, CAST(NULL AS nvarchar(4000)), p.last_batch, p.context_info
p.open_tran, CAST(NULL AS nvarchar(4000)), p.last_batch
FROM master.dbo.sysprocesses p
WHERE (p.blocked > 0 AND p.blocked <> p.spid)
OR p.spid IN (SELECT blocked FROM master.dbo.sysprocesses WHERE blocked > 0 AND blocked <> spid)

View file

@ -19,10 +19,8 @@
--
-- Bruges af: Ingen regel direkte. Styrer, hvilke databaser de databasespecifikke regler vurderes i, og hvilke
-- AX-databaser der må sende AX-fund (COM-AX-*) til SQLInsight Hub.
-- Read-only: Ja: kun SELECT på metadata, via tre-delte navne og (fra 1.4, AX-genkendelsen på SQL Server 2005 og nyere)
-- [db].sys.sp_executesql under READ COMMITTED med SET LOCK_TIMEOUT 2000, aldrig NOLOCK. Sessionen skifter
-- aldrig database, og intet ændrer data, indstillinger eller objekter. AX-genkendelsen er den samme som i
-- _collectors/AxDatabaseDetector.sql (src/SQLInsight.Core/DynamicsAx/AxDetection.cs).
-- Read-only: Ja: kun SELECT på metadata, via tre-delte navne. Sessionen skifter aldrig database, og intet ændrer
-- data, indstillinger eller objekter.
-- Kørsel: Kør i SSMS som agentens login. Hver variant er sin egen batch (GO); en variant for en anden SQL Server-version
-- end din kan fejle - det er forventet. Agenten kører altid med sessionsindstillingerne i _collectors/InstanceProbe.sql.
-- ===================================================================================================================
@ -56,25 +54,18 @@ GO
-- Variant: SQL Server 2005 og nyere: Dynamics AX-genkendelse, én gang pr. online brugerdatabase, agenten kan åbne (vist for msdb)
-- Kilde: DatabaseDiscovery.DynamicsAxSql(2005)
-- Kode: src/SQLInsight.Collectors/Discovery/DatabaseDiscovery.cs : DatabaseDiscovery.DynamicsAxSql(database, version)
EXEC [msdb].sys.sp_executesql N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'')
ORDER BY s.name, t.name, c.column_id';
SET LOCK_TIMEOUT 5000; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT COUNT(DISTINCT t.name), MAX(CASE WHEN t.name = N'SQLDICTIONARY' THEN c.precision END)
FROM [msdb].sys.tables t
LEFT JOIN [msdb].sys.columns c ON c.object_id = t.object_id AND c.name = N'RECID'
WHERE t.name IN (N'SQLDICTIONARY', N'DATAAREA', N'SYSCONFIG')
GO
-- Variant: SQL Server 2000 (legacy ODBC-sti): Dynamics AX-genkendelse, én gang pr. online brugerdatabase (vist for msdb)
-- Kilde: DatabaseDiscovery.DynamicsAxSql(2000)
-- Kode: src/SQLInsight.Collectors/Discovery/DatabaseDiscovery.cs : DatabaseDiscovery.DynamicsAxSql(database, version)
SELECT u.name, UPPER(o.name), UPPER(c.name), TYPE_NAME(c.xtype), c.xprec, c.length, 0, NULL
SELECT COUNT(DISTINCT o.name), MAX(CASE WHEN o.name = N'SQLDICTIONARY' THEN c.xprec END)
FROM [msdb].dbo.sysobjects o
JOIN [msdb].dbo.sysusers u ON u.uid = o.uid
JOIN [msdb].dbo.syscolumns c ON c.id = o.id
WHERE o.xtype = 'U' AND UPPER(o.name) IN (N'SQLDICTIONARY', N'DATAAREA', N'SYSCONFIG', N'BATCH', N'BATCHJOB', N'BATCHJOBHISTORY', N'BATCHGROUP', N'NUMBERSEQUENCETABLE', N'NUMBERSEQUENCESCOPE', N'NUMBERSEQUENCEREFERENCE', N'SYSTEMSEQUENCES', N'MODELELEMENT')
ORDER BY u.name, o.name, c.colid
LEFT JOIN [msdb].dbo.syscolumns c ON c.id = o.id AND c.name = N'RECID'
WHERE o.xtype = 'U' AND o.name IN (N'SQLDICTIONARY', N'DATAAREA', N'SYSCONFIG')
GO

View file

@ -12,8 +12,11 @@
-- GRANT VIEW SERVER SECURITY STATE TO [agent]; -- SQL Server 2022+
--
-- Bruges af: SQLI-DLK-001
-- Read-only: Ja: kun SELECT (sys.fn_xe_file_target_read_file og DMV'er) samt SET-sessionsindstillinger. Ingen
-- temp-tabeller; læsepositionen findes med ROW_NUMBER() i samme forespørgsel.
-- Read-only: IKKE STRENGT READ-ONLY. DeadlockCollector.BuildFileSql (SQL Server 2012+) opretter en midlertidig
-- tabel i tempdb (SELECT ... INTO #system_health) og sletter den igen (DROP TABLE #system_health) i
-- samme batch. Den er privat for sessionen; ingen brugerdata, indstillinger eller permanente objekter
-- ændres, men det er en skrivning i tempdb. Ring buffer-varianten (2008/2008 R2) er read-only. Øvrigt:
-- kun SELECT samt SET-sessionsindstillinger.
-- Kørsel: Kør i SSMS som agentens login. Hver variant er sin egen batch (GO); en variant for en anden SQL Server-version
-- end din kan fejle - det er forventet. Agenten kører altid med sessionsindstillingerne i _collectors/InstanceProbe.sql.
-- ===================================================================================================================
@ -21,14 +24,14 @@
-- Variant: SQL Server 2012 og nyere (system_health-filer; første læsning)
-- Kilde: DeadlockCollector.BuildFileSql
-- Kode: src/SQLInsight.Collectors/Concurrency/DeadlockCollector.cs : DeadlockCollector.BuildFileSql(lastFile: null, lastOffset: null)
SELECT e.event_data, e.file_name, e.file_offset, e.rn
FROM (
SELECT CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data,
file_name, file_offset,
ROW_NUMBER() OVER (ORDER BY file_name DESC, file_offset DESC) AS rn
FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL)
) AS e
WHERE e.event_data IS NOT NULL OR e.rn = 1;
SELECT object_name, file_name, file_offset,
CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data
INTO #system_health
FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL);
SELECT event_data FROM #system_health WHERE event_data IS NOT NULL;
SELECT TOP (1) file_name, file_offset FROM #system_health ORDER BY file_name DESC, file_offset DESC;
DROP TABLE #system_health;
GO
-- Variant: SQL Server 2008 – 2008 R2, og når event-filerne ikke kan læses

View file

@ -1,50 +0,0 @@
-- ===================================================================================================================
-- Collector: SyntheticCheckRunner (syntetiske SQL-tjek, fra 1.5)
-- ===================================================================================================================
-- Hvad: Et syntetisk SQL-tjek logger på den instans, tjekket peger på, som tjekkets login (agentens Windows-konto
-- eller et SQL-login, hvis adgangskode er krypteret til agenten), og kører tjekkets forespørgsel i en
-- transaktion, der altid rulles tilbage. Svartiden og om det lykkedes bruges af SQLI-SYN-001 og SQLI-SYN-002.
-- Fil: src/SQLInsight.Servers/Synthetic/SyntheticCheckRunner.cs (kaldt af src/SQLInsight.Agent/Servers/SyntheticCheckService.cs)
-- Interval: Tjekkets eget interval (standard 300 sek, mindst 60), og når der bedes om "Kør nu"
-- Versioner: SQL Server 2005 og nyere.
--
-- Rettigheder: Login'et i tjekket skal kunne logge ind og køre forespørgslen. Til standardforespørgslen (SELECT 1) er
-- det nok at kunne logge ind (og CONNECT til databasen, hvis tjekket har en).
--
-- Bruges af: SQLI-SYN-001, SQLI-SYN-002.
-- Read-only: Ja. Forespørgslen valideres som én skrivebeskyttet SELECT, når tjekket gemmes og før hver kørsel
-- (src/SQLInsight.Core/CustomRules/ReadOnlyQuery.cs: ingen andre sætninger, ingen INTO, EXEC, sekvenser
-- eller låse-hints), og køres i en transaktion, der altid rulles tilbage. Højst 10.000 rækker læses.
-- Kørsel: Kør i SSMS som tjekkets login. Hver GO er en separat kommando på samme forbindelse; ret [msdb] til
-- tjekkets database, eller udelad USE, hvis tjekket ingen database har.
-- ===================================================================================================================
-- Variant: Alle versioner: sættes på hver forbindelse, agenten åbner (også tjekkets)
-- Kilde: SqlConnectionFactory.SessionSettings
-- Kode: src/SQLInsight.Connectivity/SqlConnectionFactory.cs : SqlConnectionFactory.SessionSettings
SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
-- Variant: Kun når tjekket har en database (vist for msdb)
-- Kilde: SyntheticCheckRunner.UseDatabaseSql
-- Kode: src/SQLInsight.Servers/Synthetic/SyntheticCheckRunner.cs : SyntheticCheckRunner.UseDatabaseSql(database)
USE [msdb];
GO
-- Variant: Alle versioner fra SQL Server 2005: før forespørgslen
-- Kilde: SyntheticCheckRunner.BeginTransactionSql
-- Kode: src/SQLInsight.Servers/Synthetic/SyntheticCheckRunner.cs : SyntheticCheckRunner.BeginTransactionSql
BEGIN TRANSACTION;
GO
-- Variant: Standardforespørgslen, når tjekket ikke har sin egen (en egen forespørgsel køres her i stedet, højst 10.000 rækker læses)
-- Kilde: SyntheticCheckSettings.DefaultQuery
-- Kode: src/SQLInsight.Core/SyntheticChecks/SyntheticCheckDefinitions.cs : SyntheticCheckSettings.DefaultQuery
SELECT 1
GO
-- Variant: Alle versioner fra SQL Server 2005: altid efter forespørgslen, også når den fejler
-- Kilde: SyntheticCheckRunner.RollbackSql
-- Kode: src/SQLInsight.Servers/Synthetic/SyntheticCheckRunner.cs : SyntheticCheckRunner.RollbackSql
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
GO

View file

@ -9,7 +9,6 @@
<Project Path="src/SQLInsight.Hub/SQLInsight.Hub.csproj" />
<Project Path="src/SQLInsight.Licensing/SQLInsight.Licensing.csproj" />
<Project Path="src/SQLInsight.Reports/SQLInsight.Reports.csproj" />
<Project Path="src/SQLInsight.Servers/SQLInsight.Servers.csproj" />
<Project Path="src/SQLInsight.Updates/SQLInsight.Updates.csproj" />
<Project Path="src/SQLInsight.Notifications/SQLInsight.Notifications.csproj" />
<Project Path="src/SQLInsight.Repository/SQLInsight.Repository.csproj" />
@ -26,7 +25,6 @@
<Project Path="tests/SQLInsight.Licensing.Tests/SQLInsight.Licensing.Tests.csproj" />
<Project Path="tests/SQLInsight.Notifications.Tests/SQLInsight.Notifications.Tests.csproj" />
<Project Path="tests/SQLInsight.Rules.Tests/SQLInsight.Rules.Tests.csproj" />
<Project Path="tests/SQLInsight.Servers.Tests/SQLInsight.Servers.Tests.csproj" />
<Project Path="tests/SQLInsight.Updates.Tests/SQLInsight.Updates.Tests.csproj" />
</Folder>
<Folder Name="/tools/">

View file

@ -1,101 +0,0 @@
/*
SQLInsight Pro: valgfri læseadgang til Dynamics AX' systemtabeller
=====================================================================
TILVALG. Agenten læser normalt kun metadata (grant-least-privilege.sql). Med dette script må den desuden læse de
få AX-systemtabeller, som AX-tjekkene bruger, i ÉN navngiven AX-database, så SQLInsight kan advare om batchjob i
fejl, forsinkede eller hængende batchjob, nummerserier og RecId'er der er ved at løbe tør:
Axapta 3.0 og AX 4.0: BATCH, NUMBERSEQUENCETABLE, NUMBERSEQUENCEREFERENCE, SYSTEMSEQUENCES, SQLDICTIONARY, DATAAREA
AX 2009: BATCHJOB, BATCHJOBHISTORY, NUMBERSEQUENCETABLE, NUMBERSEQUENCEREFERENCE, SYSTEMSEQUENCES,
SQLDICTIONARY, DATAAREA
AX 2012 og F&O: BATCHJOB, BATCHJOBHISTORY, NUMBERSEQUENCETABLE, NUMBERSEQUENCESCOPE, NUMBERSEQUENCEREFERENCE,
SYSTEMSEQUENCES, SQLDICTIONARY, DATAAREA
Kun SELECT, kun disse tabeller: ingen kundedata (debitorer, ordrer, posteringer), ingen skriverettigheder. Scriptet
finder selv AX-versionen og skemaet, tabellerne ligger i (bmssa i Axapta 3.0, dbo senere). Tabeller, der ikke findes
i databasen, springes over. Scriptet kan køres igen uden skade. Fjern adgangen igen med revoke-dynamics-ax-read.sql.
SQLInsight Web viser det samme som færdige GRANT-sætninger for hver AX-database (Dynamics AX → AX-databaser).
AX 2009, 2012 og F&O er baseret på Microsofts dokumenterede skema – ikke verificeret mod kundedata.
Understøtter SQL Server 2005 og nyere. Kør som sysadmin eller som db_owner/db_securityadmin i AX-databasen.
*/
SET NOCOUNT ON;
DECLARE @DatabaseName sysname;
DECLARE @LoginName sysname;
SET @DatabaseName = N'AXDB'; -- <== ret til AX-databasen
SET @LoginName = N'DOMÆNE\svc-sqlinsight$'; -- <== ret til agentens login
IF DB_ID(@DatabaseName) IS NULL
BEGIN
RAISERROR(N'Databasen %s findes ikke.', 16, 1, @DatabaseName);
RETURN;
END;
IF SUSER_SID(@LoginName) IS NULL
BEGIN
RAISERROR(N'Login %s findes ikke. Kør grant-least-privilege.sql først.', 16, 1, @LoginName);
RETURN;
END;
DECLARE @Procedure nvarchar(300);
DECLARE @Sql nvarchar(max);
SET @Procedure = QUOTENAME(@DatabaseName) + N'.sys.sp_executesql';
SET @Sql = N'
SET NOCOUNT ON;
DECLARE @schema sysname;
DECLARE @precision int;
DECLARE @version nvarchar(10);
DECLARE @tables nvarchar(400);
DECLARE @user sysname;
DECLARE @grant nvarchar(max);
DECLARE @count int;
SELECT TOP 1 @schema = s.name, @precision = c.precision
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
LEFT JOIN sys.columns c ON c.object_id = t.object_id AND UPPER(c.name) = N''RECID''
WHERE UPPER(t.name) = N''SQLDICTIONARY'' AND OBJECT_ID(QUOTENAME(s.name) + N''.DATAAREA'', N''U'') IS NOT NULL
ORDER BY CASE WHEN s.name = N''dbo'' THEN 0 ELSE 1 END, s.name;
IF @schema IS NULL
BEGIN
DECLARE @database sysname;
SET @database = DB_NAME();
RAISERROR(N''%s er ikke en Dynamics AX-database (SQLDICTIONARY og DATAAREA findes ikke i samme skema). Intet er ændret.'', 16, 1, @database);
RETURN;
END;
/* The same version rules as SQLInsight''s detection (Core/DynamicsAx/AxDetection.cs). */
IF @precision <= 10 SET @version = N''3.0'';
ELSE IF OBJECT_ID(QUOTENAME(@schema) + N''.BATCHJOB'', N''U'') IS NULL SET @version = N''4.0'';
ELSE IF OBJECT_ID(QUOTENAME(@schema) + N''.NUMBERSEQUENCESCOPE'', N''U'') IS NULL SET @version = N''2009'';
ELSE SET @version = N''2012'';
/* The tables per version: the same lists as the version adapters'' ReadTables (Core/DynamicsAx/AxSchemaAdapter.cs). */
SET @tables = CASE
WHEN @version IN (N''3.0'', N''4.0'') THEN N''|BATCH|NUMBERSEQUENCETABLE|NUMBERSEQUENCEREFERENCE|SYSTEMSEQUENCES|SQLDICTIONARY|DATAAREA|''
WHEN @version = N''2009'' THEN N''|BATCHJOB|BATCHJOBHISTORY|NUMBERSEQUENCETABLE|NUMBERSEQUENCEREFERENCE|SYSTEMSEQUENCES|SQLDICTIONARY|DATAAREA|''
ELSE N''|BATCHJOB|BATCHJOBHISTORY|NUMBERSEQUENCETABLE|NUMBERSEQUENCESCOPE|NUMBERSEQUENCEREFERENCE|SYSTEMSEQUENCES|SQLDICTIONARY|DATAAREA|''
END;
SELECT @user = name FROM sys.database_principals WHERE sid = SUSER_SID(@login);
IF @user IS NULL
BEGIN
SET @user = @login;
SET @grant = N''CREATE USER '' + QUOTENAME(@user) + N'' FOR LOGIN '' + QUOTENAME(@login) + N'';'';
EXEC (@grant);
PRINT N''Bruger oprettet uden rettigheder: '' + @user;
END;
SET @grant = N'''';
SET @count = 0;
SELECT @grant = @grant + N''GRANT SELECT ON '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) + N'' TO '' + QUOTENAME(@user) + N''; '', @count = @count + 1
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
WHERE s.name = @schema AND CHARINDEX(N''|'' + UPPER(t.name) + N''|'', @tables) > 0;
IF @count > 0 EXEC (@grant);
PRINT N''Dynamics AX '' + @version + N'' i skemaet '' + @schema + N'': SELECT givet på '' + CAST(@count AS nvarchar(10)) + N'' tabeller i '' + DB_NAME() + N'' til '' + @user + N''.'';';
EXEC @Procedure @Sql, N'@login sysname', @login = @LoginName;

View file

@ -1,467 +0,0 @@
<#
SQLInsight Pro: least-privilege rettigheder til agentens konto på en Windows-server
=====================================================================================
Giver SQLInsight-agentens konto præcis den læseadgang, den skal bruge for at overvåge denne server:
CPU, hukommelse, diske, tjenester, hændelseslog og planlagte opgaver. Aldrig lokal administrator, og
aldrig ret til at køre kode på serveren (heller ikke PowerShell Remoting).
Scriptet ændrer kun adgangsrettigheder: det starter, stopper eller ændrer ingen tjenester, opgaver eller
indstillinger, og det læser ingen data.
Afprøv det først på en server, der ikke er i produktion.
Kør som administrator på den server, der skal overvåges (Windows PowerShell 2.0 eller nyere):
.\grant-windows-server.ps1 -Account 'DOMÆNE\AGENTSERVER$' -WhatIf # vis, hvad der ville ske; ændrer intet
.\grant-windows-server.ps1 -Account 'DOMÆNE\AGENTSERVER$'
.\grant-windows-server.ps1 -Account 'DOMÆNE\AGENTSERVER$' -WinRM -EnableFirewall -AgentAddress 10.0.0.15
Fjern præcis det, scriptet har givet, igen:
.\grant-windows-server.ps1 -Account 'DOMÆNE\AGENTSERVER$' -Remove
Agentens konto: kører agenten som NT SERVICE\SQLInsight Agent, LocalSystem eller NETWORK SERVICE, kommer den
til andre servere som computerkontoen DOMÆNE\COMPUTERNAVN$. En gMSA eller domænekonto er den samme overalt.
Hvad der gives, og hvorfor:
Performance Monitor Users ydelsestællere: CPU, hukommelse, paging
Event Log Readers hændelseslogge, der tillader gruppen (System, Application m.fl.; aldrig Security)
Distributed COM Users WMI/CIM over DCOM (RPC)
WMI: Enable Account + Remote Enable (0x21) på root\cimv2 og root\Microsoft\Windows\TaskScheduler, kun på
selve navnerummet (ikke arvet). Ingen skrive- eller metoderet ud over Windows'
standard.
Service Control Manager forbinde, liste tjenester, læse låsestatus og sikkerhed (CC LC RP RC) - det
samme, som en lokalt logget på bruger har. Ingen ret til at starte eller stoppe.
Kun med -WinRM læseret (GENERIC_READ, ingen Execute) i WinRM's RootSDDL, så WMI kan læses over
WinRM (TCP 5985). Kontoen kommer IKKE i Remote Management Users, for den gruppe
giver også PowerShell Remoting. Uden -WinRM bruger agenten DCOM.
Hvad der IKKE gives: lokal administrator, PowerShell Remoting/WinRS, ret til at starte/stoppe tjenester,
skrive i WMI, læse filer eller Security-loggen.
Firewall (kun med -EnableFirewall og -AgentAddress, Windows Server 2012 og nyere): opretter scriptets egne regler
"SQLInsight Agent: ..." for RPC (WMI over DCOM og hændelseslog) og med -WinRM for TCP 5985, kun fra agentens
adresse. Windows' indbyggede regler røres ikke.
Sikkerhedsnet:
- Intet ændres, før alt er læst og kontrolleret (konto, grupper, sikkerhed, firewall). Fejler kontrollen, er
intet ændret.
- Før første ændring gemmes de oprindelige indstillinger (SDDL for Service Control Manager, WMI-navnerummene og
WinRM, gruppemedlemskaber og firewallregler) i %ProgramData%\SQLInsight\grant-windows-server\<SID>-<tid>-foer.xml.
- Hvad scriptet selv har tilføjet, står i %ProgramData%\SQLInsight\grant-windows-server\<SID>.xml (kun
administratorer har adgang til mappen). -Remove fjerner kun det; intet, der fandtes før.
- Har kontoen allerede en rettighed, røres den ikke (og fjernes ikke med -Remove). Har den en "Nægt"-regel,
stopper scriptet uden at ændre noget.
- Hvert skridt kontrolleres bagefter. Fejler et skridt, står det, der allerede er gjort, i tilstandsfilen, så
scriptet kan køres igen eller med -Remove.
- Manuel gendannelse af Service Control Manager: sc.exe sdset scmanager "<SDDL fra -foer.xml>".
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string] $Account = 'DOMÆNE\AGENTSERVER$', # <== agentens konto
[switch] $Remove,
[switch] $WinRM,
[switch] $EnableFirewall,
[string] $AgentAddress
)
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
$placeholder = 'DOMÆNE\AGENTSERVER$'
$whatIf = [bool] $WhatIfPreference
# ---- 1. Kontrol: intet ændres, før alt her er i orden ---------------------------------------------------------
$principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Kør scriptet i en PowerShell, der er startet som administrator ("Kør som administrator"). Intet er ændret.'
}
if ([string]::IsNullOrEmpty($Account) -or $Account -eq $placeholder) {
throw "Angiv agentens konto, fx -Account 'DOMÆNE\AGENTSERVER`$' (SQLInsight Web viser den under serveren). Intet er ændret."
}
foreach ($c in $Account.ToCharArray()) {
if ([char]::IsControl($c)) { throw 'Kontonavnet indeholder et kontroltegn. Intet er ændret.' }
}
try {
$sidObj = (New-Object Security.Principal.NTAccount($Account)).Translate([Security.Principal.SecurityIdentifier])
} catch {
throw "Kontoen '$Account' kunne ikke slås op (findes den, og kan serveren nå domænet?). Intet er ændret."
}
$sid = $sidObj.Value
if ($sid -eq 'S-1-5-18') {
Write-Host 'Agenten kører som LocalSystem på denne server og har allerede adgang. Intet at gøre.'
return
}
# Grupper med mange medlemmer: rettighederne skal gives til agentens egen konto, aldrig til alle.
$broad = @('S-1-1-0', 'S-1-5-7', 'S-1-5-2', 'S-1-5-4', 'S-1-5-11', 'S-1-5-32-544', 'S-1-5-32-545', 'S-1-5-32-546')
if (($broad -contains $sid) -or ($sid -match '^S-1-5-21-.+-(512|513|514|515|516|519)$')) {
throw "'$Account' er en bred gruppe. Giv rettighederne til agentens egen konto (gMSA, domænekonto eller computerkonto). Intet er ændret."
}
if ($EnableFirewall -and -not $Remove) {
if ([string]::IsNullOrEmpty($AgentAddress)) {
throw 'Med -EnableFirewall skal -AgentAddress angives (agentens IP-adresse eller net, fx 10.0.0.15 eller 10.0.0.0/24), så portene kun åbnes for agenten. Intet er ændret.'
}
foreach ($a in $AgentAddress.Split(',')) {
$ip = $null
if (-not [Net.IPAddress]::TryParse($a.Trim().Split('/')[0], [ref] $ip)) {
throw "-AgentAddress '$a' er ikke en IP-adresse eller et net (fx 10.0.0.15 eller 10.0.0.0/24). Intet er ændret."
}
}
if (-not (Get-Command New-NetFirewallRule -ErrorAction SilentlyContinue)) {
throw '-EnableFirewall kræver Windows Server 2012 eller nyere. Åbn portene manuelt (se docs/windows-servere.md). Intet er ændret.'
}
} elseif ($AgentAddress -and -not $Remove) {
Write-Host ' (-AgentAddress bruges kun sammen med -EnableFirewall)'
}
$stateDir = Join-Path $env:ProgramData 'SQLInsight\grant-windows-server'
$statePath = Join-Path $stateDir "$sid.xml"
if (Test-Path $statePath) {
$state = Import-Clixml $statePath
} else {
$state = @{ Sid = $sid; Account = $Account; Groups = @(); Wmi = @(); Scm = $false; WinRMRoot = $false; Firewall = @() }
}
if ($Remove) { $verb = 'Fjerner det, scriptet har givet' } else { $verb = 'Giver rettigheder til' }
Write-Host "$verb $Account ($sid) på $env:COMPUTERNAME"
if ($Remove -and -not (Test-Path $statePath)) {
Write-Host "Intet at fjerne: scriptet har ikke givet $Account noget på denne server ($statePath findes ikke)."
return
}
# ---- Hjælpefunktioner (sikkerhedsbeskrivelser som SDDL, ændret via .NET's RawSecurityDescriptor) ---------------
$accessAllowed = [Security.AccessControl.AceQualifier]::AccessAllowed
$accessDenied = [Security.AccessControl.AceQualifier]::AccessDenied
$allSections = [Security.AccessControl.AccessControlSections]::All
function Get-Aces([string] $Sddl) {
$raw = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Sddl
if ($raw.DiscretionaryAcl -eq $null) { return @() }
return @($raw.DiscretionaryAcl | Where-Object { $_ -is [Security.AccessControl.CommonAce] -and $_.SecurityIdentifier -eq $sidObj })
}
# Har kontoen en Nægt-regel? Så stopper scriptet: den er sat med vilje.
function Test-Denied([string] $Sddl) {
return @(Get-Aces $Sddl | Where-Object { $_.AceQualifier -eq $accessDenied }).Count -gt 0
}
# Har kontoen allerede (mindst) disse rettigheder fra en Tillad-regel?
function Test-Allowed([string] $Sddl, [int] $Mask) {
return @(Get-Aces $Sddl | Where-Object { $_.AceQualifier -eq $accessAllowed -and (($_.AccessMask -band $Mask) -eq $Mask) }).Count -gt 0
}
# Fjerner præcis én Tillad-regel for kontoen med netop denne maske, ikke arvet og uden flag (den, scriptet satte).
function Remove-Ace([string] $Sddl, [int] $Mask) {
$raw = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Sddl
if ($raw.DiscretionaryAcl -eq $null) { return $Sddl }
for ($i = 0; $i -lt $raw.DiscretionaryAcl.Count; $i++) {
$ace = $raw.DiscretionaryAcl[$i]
if ($ace -is [Security.AccessControl.CommonAce] -and $ace.SecurityIdentifier -eq $sidObj -and $ace.AceQualifier -eq $accessAllowed `
-and $ace.AccessMask -eq $Mask -and $ace.AceFlags -eq [Security.AccessControl.AceFlags]::None) {
$raw.DiscretionaryAcl.RemoveAce($i)
return $raw.GetSddlForm($allSections)
}
}
return $Sddl
}
# Indsætter præcis én Tillad-regel (ikke arvet) efter de eksplicitte regler og før de arvede (kanonisk orden).
function Add-Ace([string] $Sddl, [int] $Mask) {
$raw = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Sddl
if ($raw.DiscretionaryAcl -eq $null) { throw 'Sikkerhedsbeskrivelsen har ingen DACL; den ændres ikke.' }
$ace = New-Object Security.AccessControl.CommonAce -ArgumentList @([Security.AccessControl.AceFlags]::None, $accessAllowed, $Mask, $sidObj, $false, $null)
$at = 0
while ($at -lt $raw.DiscretionaryAcl.Count -and -not $raw.DiscretionaryAcl[$at].IsInherited) { $at++ }
$raw.DiscretionaryAcl.InsertAce($at, $ace)
$new = $raw.GetSddlForm($allSections)
# Kontrol: fjernes den nye regel igen, er resten præcis som før.
$before = (New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Sddl).GetSddlForm($allSections)
if ((Remove-Ace $new $Mask) -ne $before) {
throw 'Intern kontrol fejlede: den nye sikkerhedsbeskrivelse adskiller sig fra den gamle med mere end én regel.'
}
return $new
}
function Read-NamespaceSddl([string] $Namespace) {
$security = Get-WmiObject -Namespace $Namespace -Class __SystemSecurity
$binary = @($null)
$rc = $security.PsBase.InvokeMethod('GetSD', $binary)
if ($rc -ne 0) { throw "Kunne ikke læse sikkerheden på WMI-navnerummet $Namespace ($rc)." }
return (New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList @($binary[0], 0)).GetSddlForm($allSections)
}
function Write-NamespaceSddl([string] $Namespace, [string] $Sddl) {
$raw = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Sddl
$bytes = New-Object byte[] $raw.BinaryLength
$raw.GetBinaryForm($bytes, 0)
$security = Get-WmiObject -Namespace $Namespace -Class __SystemSecurity
$rc = $security.PsBase.InvokeMethod('SetSD', @(, $bytes))
if ($rc -ne 0) { throw "Kunne ikke sætte sikkerheden på WMI-navnerummet $Namespace ($rc)." }
}
function Read-ScmSddl {
$sddl = ((& sc.exe sdshow scmanager) | Where-Object { $_ -match '\S' }) -join ''
if ($LASTEXITCODE -ne 0 -or $sddl -notmatch '^D:') { throw "Kunne ikke læse Service Control Managers sikkerhed: $sddl" }
return $sddl
}
function Write-ScmSddl([string] $Sddl) {
$output = & sc.exe sdset scmanager $Sddl
if ($LASTEXITCODE -ne 0) { throw "sc.exe sdset scmanager fejlede ($LASTEXITCODE): $output" }
}
function Get-GroupMembers($Group) {
return @($Group.Invoke('Members') | ForEach-Object {
$bytes = $_.GetType().InvokeMember('objectSid', 'GetProperty', $null, $_, $null)
(New-Object Security.Principal.SecurityIdentifier($bytes, 0)).Value
})
}
# Medlemmer tilføjes og fjernes via deres SID (virker også for NT SERVICE\..., gMSA og computerkonti), ellers via navnet.
function Set-GroupMember($Group, [bool] $Add) {
$paths = @("WinNT://$sid", 'WinNT://' + $Account.Replace('\', '/'))
foreach ($path in $paths) {
try {
if ($Add) { $Group.Add($path) } else { $Group.Remove($path) }
return
} catch {
if ($path -eq $paths[-1]) { throw }
}
}
}
function Save-State {
if ($whatIf) { return }
if (-not (Test-Path $stateDir)) { New-Item -ItemType Directory -Force -Path $stateDir | Out-Null }
# Kun administratorer og SYSTEM: filen styrer, hvad -Remove fjerner.
$acl = New-Object Security.AccessControl.DirectorySecurity
$acl.SetAccessRuleProtection($true, $false)
foreach ($who in 'S-1-5-32-544', 'S-1-5-18') {
$acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule((New-Object Security.Principal.SecurityIdentifier($who)), 'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow')))
}
Set-Acl -Path $stateDir -AclObject $acl
$state | Export-Clixml -Path $statePath
}
# ---- 2. Læs nuværende tilstand og læg en plan (ændrer intet) ---------------------------------------------------
$wmiMask = 0x21 # Enable Account (0x1) + Remote Enable (0x20)
$scmMask = 0x20015 # CC (0x1) + LC (0x4) + RP (0x10) + RC (READ_CONTROL 0x20000)
$winrmMask = [int] 0x80000000 # GENERIC_READ (GR), ingen Execute (GX)
$plan = New-Object Collections.ArrayList
$original = @{ Time = (Get-Date).ToString('o'); Account = $Account; Sid = $sid; Groups = @{}; Wmi = @{}; Scm = $null; WinRMRoot = $null; Firewall = @{} }
# Lokale grupper, fundet via deres SID, så det virker på dansk, engelsk og andre sprog.
$groups = @(
@('S-1-5-32-558', 'ydelsestællere (CPU, hukommelse, paging)'),
@('S-1-5-32-573', 'hændelseslogge (aldrig Security)'),
@('S-1-5-32-562', 'WMI over DCOM')
)
foreach ($g in $groups) {
try {
$groupName = (New-Object Security.Principal.SecurityIdentifier($g[0])).Translate([Security.Principal.NTAccount]).Value.Split('\')[1]
} catch {
Write-Host " - gruppen $($g[0]) ($($g[1])) findes ikke på denne Windows-version; springes over"
continue
}
$group = [ADSI]("WinNT://$env:COMPUTERNAME/$groupName,group")
$isMember = (Get-GroupMembers $group) -contains $sid
$original.Groups[$g[0]] = $isMember
$added = @($state.Groups) -contains $g[0]
if ($Remove) {
if ($added -and $isMember) { [void] $plan.Add(@{ Kind = 'GroupRemove'; Sid = $g[0]; Name = $groupName; Text = "fjern fra gruppen $groupName" }) }
elseif ($added) { $state.Groups = @($state.Groups | Where-Object { $_ -ne $g[0] }); Write-Host " = ${groupName}: allerede fjernet" }
elseif ($isMember) { Write-Host " = ${groupName}: var medlem før scriptet; bevares" }
} elseif ($isMember) {
Write-Host " = ${groupName}: allerede medlem"
} else {
[void] $plan.Add(@{ Kind = 'GroupAdd'; Sid = $g[0]; Name = $groupName; Text = "medlem af $groupName ($($g[1]))" })
}
}
# WMI-navnerum.
$namespaces = @('root\cimv2')
if (Get-WmiObject -Namespace 'root\Microsoft\Windows' -Class __Namespace -Filter "Name='TaskScheduler'" -ErrorAction SilentlyContinue) {
$namespaces += 'root\Microsoft\Windows\TaskScheduler'
} elseif (-not $Remove) {
Write-Host ' - root\Microsoft\Windows\TaskScheduler findes ikke (før Windows Server 2012): planlagte opgaver kan ikke måles'
}
foreach ($ns in $namespaces) {
$sddl = Read-NamespaceSddl $ns
$original.Wmi[$ns] = $sddl
if ($Remove) {
if (@($state.Wmi) -contains $ns) { [void] $plan.Add(@{ Kind = 'WmiRemove'; Namespace = $ns; Text = "fjern WMI-adgang på $ns" }) }
} elseif (Test-Denied $sddl) {
throw "Kontoen har en Nægt-regel på WMI-navnerummet $ns. Den er sat med vilje; fjern den først, hvis agenten skal læse herfra. Intet er ændret."
} elseif (Test-Allowed $sddl $wmiMask) {
Write-Host " = WMI ${ns}: har allerede adgang"
} else {
[void] (Add-Ace $sddl $wmiMask) # kontrollerer, at ændringen er præcis én regel
[void] $plan.Add(@{ Kind = 'WmiAdd'; Namespace = $ns; Text = "WMI $ns (Enable Account, Remote Enable)" })
}
}
# Service Control Manager: over netværket må ellers kun administratorer liste tjenester.
$scm = Read-ScmSddl
$original.Scm = $scm
if ($Remove) {
if ($state.Scm) { [void] $plan.Add(@{ Kind = 'ScmRemove'; Text = 'fjern læseadgang til Service Control Manager' }) }
} elseif (Test-Denied $scm) {
throw 'Kontoen har en Nægt-regel på Service Control Manager. Den er sat med vilje; fjern den først, hvis agenten skal se tjenester. Intet er ændret.'
} elseif (Test-Allowed $scm $scmMask) {
Write-Host ' = Service Control Manager: har allerede læseadgang'
} else {
[void] (Add-Ace $scm $scmMask)
[void] $plan.Add(@{ Kind = 'ScmAdd'; Text = 'Service Control Manager (CC LC RP RC: forbinde, liste, læse)' })
}
# WinRM (kun med -WinRM, eller når -Remove skal fjerne det, scriptet gav).
if (($WinRM -and -not $Remove) -or ($Remove -and $state.WinRMRoot)) {
$service = Get-Service -Name WinRM -ErrorAction SilentlyContinue
if (-not $service -or $service.Status -ne 'Running') {
throw 'WinRM-tjenesten kører ikke. Start den (winrm quickconfig), eller kør uden -WinRM, så agenten bruger DCOM. Intet er ændret.'
}
$root = (Get-Item WSMan:\localhost\Service\RootSDDL).Value
$original.WinRMRoot = $root
if ($Remove) {
[void] $plan.Add(@{ Kind = 'WinRMRemove'; Text = 'fjern læseadgang i WinRM (RootSDDL)' })
} elseif (Test-Denied $root) {
throw 'Kontoen har en Nægt-regel i WinRM (RootSDDL). Kør uden -WinRM, så agenten bruger DCOM. Intet er ændret.'
} elseif (Test-Allowed $root $winrmMask) {
Write-Host ' = WinRM: har allerede læseadgang'
} else {
[void] (Add-Ace $root $winrmMask)
[void] $plan.Add(@{ Kind = 'WinRMAdd'; Text = 'WinRM (RootSDDL): læseret (GR), ingen Execute' })
}
}
# Firewall: scriptets egne regler, kun fra agentens adresse. Windows' indbyggede regler røres ikke.
$rules = @(
@{ Name = 'SQLInsight-Agent-RPC-EPMAP'; Display = 'SQLInsight Agent: RPC Endpoint Mapper (WMI over DCOM, hændelseslog)'; Port = 'RPCEPMap'; Service = 'rpcss' },
@{ Name = 'SQLInsight-Agent-WMI-DCOM'; Display = 'SQLInsight Agent: WMI over DCOM'; Port = 'RPC'; Service = 'winmgmt' },
@{ Name = 'SQLInsight-Agent-EventLog'; Display = 'SQLInsight Agent: hændelseslog (RPC)'; Port = 'RPC'; Service = 'eventlog' }
)
if ($WinRM) { $rules += @{ Name = 'SQLInsight-Agent-WinRM'; Display = 'SQLInsight Agent: WinRM (HTTP 5985)'; Port = '5985'; Service = $null } }
if ($Remove -and @($state.Firewall).Count -gt 0 -and (Get-Command Get-NetFirewallRule -ErrorAction SilentlyContinue)) {
foreach ($name in @($state.Firewall)) {
$existing = Get-NetFirewallRule -Name $name -ErrorAction SilentlyContinue
if ($existing) { $original.Firewall[$name] = @(($existing | Get-NetFirewallAddressFilter).RemoteAddress) }
if ($existing) { [void] $plan.Add(@{ Kind = 'FirewallRemove'; Name = $name; Text = "fjern firewallreglen $name" }) }
}
} elseif ($EnableFirewall -and -not $Remove) {
$wanted = @($AgentAddress.Split(',') | ForEach-Object { $_.Trim() })
foreach ($r in $rules) {
$existing = Get-NetFirewallRule -Name $r.Name -ErrorAction SilentlyContinue
if ($existing) {
$current = @(($existing | Get-NetFirewallAddressFilter).RemoteAddress)
$original.Firewall[$r.Name] = $current
if (($current -join ',') -eq ($wanted -join ',')) { Write-Host " = firewall $($r.Name): uændret"; continue }
[void] $plan.Add(@{ Kind = 'FirewallUpdate'; Rule = $r; Address = $wanted; Text = "firewallreglen $($r.Name): kun fra $AgentAddress" })
} else {
[void] $plan.Add(@{ Kind = 'FirewallAdd'; Rule = $r; Address = $wanted; Text = "firewall: $($r.Display) (TCP $($r.Port)), kun fra $AgentAddress" })
}
}
}
if ($plan.Count -eq 0) {
if ($Remove -and -not $whatIf) { Save-State }
Write-Host 'Alt er allerede på plads. Intet er ændret.'
return
}
# ---- 3. Gem det oprindelige, og udfør planen ét skridt ad gangen -----------------------------------------------
$backupPath = Join-Path $stateDir ("$sid-" + (Get-Date).ToString('yyyyMMdd-HHmmss') + '-foer.xml')
if (-not $whatIf) {
Save-State
$original | Export-Clixml -Path $backupPath
Write-Host " Oprindelige indstillinger gemt i $backupPath"
}
foreach ($step in $plan) {
if (-not $PSCmdlet.ShouldProcess($env:COMPUTERNAME, $step.Text)) { continue }
try {
switch ($step.Kind) {
'GroupAdd' {
$group = [ADSI]("WinNT://$env:COMPUTERNAME/$($step.Name),group")
Set-GroupMember $group $true
if (-not ((Get-GroupMembers $group) -contains $sid)) { throw "kontoen kom ikke i gruppen $($step.Name)" }
$state.Groups = @($state.Groups) + $step.Sid
}
'GroupRemove' {
$group = [ADSI]("WinNT://$env:COMPUTERNAME/$($step.Name),group")
Set-GroupMember $group $false
$state.Groups = @($state.Groups | Where-Object { $_ -ne $step.Sid })
}
'WmiAdd' {
Write-NamespaceSddl $step.Namespace (Add-Ace (Read-NamespaceSddl $step.Namespace) $wmiMask)
if (-not (Test-Allowed (Read-NamespaceSddl $step.Namespace) $wmiMask)) { throw "adgangen på $($step.Namespace) blev ikke sat" }
$state.Wmi = @($state.Wmi) + $step.Namespace
}
'WmiRemove' {
$sddl = Read-NamespaceSddl $step.Namespace
$new = Remove-Ace $sddl $wmiMask
if ($new -ne $sddl) { Write-NamespaceSddl $step.Namespace $new }
$state.Wmi = @($state.Wmi | Where-Object { $_ -ne $step.Namespace })
}
'ScmAdd' {
Write-ScmSddl (Add-Ace (Read-ScmSddl) $scmMask)
if (-not (Test-Allowed (Read-ScmSddl) $scmMask)) { throw 'læseadgangen til Service Control Manager blev ikke sat' }
$state.Scm = $true
}
'ScmRemove' {
$sddl = Read-ScmSddl
$new = Remove-Ace $sddl $scmMask
if ($new -ne $sddl) { Write-ScmSddl $new }
$state.Scm = $false
}
'WinRMAdd' {
$new = Add-Ace (Get-Item WSMan:\localhost\Service\RootSDDL).Value $winrmMask
Set-Item -Path WSMan:\localhost\Service\RootSDDL -Value $new -Force -WhatIf:$false -Confirm:$false
if (-not (Test-Allowed (Get-Item WSMan:\localhost\Service\RootSDDL).Value $winrmMask)) { throw 'læseretten i WinRM blev ikke sat' }
$state.WinRMRoot = $true
}
'WinRMRemove' {
$sddl = (Get-Item WSMan:\localhost\Service\RootSDDL).Value
$new = Remove-Ace $sddl $winrmMask
if ($new -ne $sddl) { Set-Item -Path WSMan:\localhost\Service\RootSDDL -Value $new -Force -WhatIf:$false -Confirm:$false }
$state.WinRMRoot = $false
}
'FirewallAdd' {
$r = $step.Rule
$arguments = @{ Name = $r.Name; DisplayName = $r.Display; Direction = 'Inbound'; Action = 'Allow'; Protocol = 'TCP'; LocalPort = $r.Port; RemoteAddress = $step.Address; Profile = 'Any' }
if ($r.Service) { $arguments.Program = '%SystemRoot%\system32\svchost.exe'; $arguments.Service = $r.Service }
New-NetFirewallRule @arguments -WhatIf:$false -Confirm:$false | Out-Null
$state.Firewall = @($state.Firewall) + $r.Name
}
'FirewallUpdate' {
Set-NetFirewallRule -Name $step.Rule.Name -RemoteAddress $step.Address -WhatIf:$false -Confirm:$false
if (-not (@($state.Firewall) -contains $step.Rule.Name)) { $state.Firewall = @($state.Firewall) + $step.Rule.Name }
}
'FirewallRemove' {
Remove-NetFirewallRule -Name $step.Name -WhatIf:$false -Confirm:$false
$state.Firewall = @($state.Firewall | Where-Object { $_ -ne $step.Name })
}
}
} catch {
Save-State
throw "Fejlede ved '$($step.Text)': $($_.Exception.Message) Det, der allerede er gjort, står i $statePath; ret fejlen og kør scriptet igen, eller kør det med -Remove. De oprindelige indstillinger står i $backupPath."
}
Save-State
if ($Remove) { Write-Host " - $($step.Text)" } else { Write-Host " + $($step.Text)" }
}
if ($whatIf) { return }
if ($Remove) {
$left = @($state.Groups).Count + @($state.Wmi).Count + @($state.Firewall).Count
if ($state.Scm) { $left++ }
if ($state.WinRMRoot) { $left++ }
if ($left -eq 0) {
Move-Item -Path $statePath -Destination (Join-Path $stateDir ("$sid-" + (Get-Date).ToString('yyyyMMdd-HHmmss') + '-fjernet.xml'))
}
Write-Host 'Færdig. Det, scriptet gav, er fjernet; det, der fandtes før, er bevaret.'
} else {
Write-Host 'Færdig. Test forbindelsen fra SQLInsight (Servere > Windows-servere > serveren > Test forbindelsen).'
}

View file

@ -1,66 +0,0 @@
/*
SQLInsight Pro: fjern den valgfri læseadgang til Dynamics AX' systemtabeller
==============================================================================
Fjerner det, grant-dynamics-ax-read.sql gav, i én AX-database: SELECT på AX' batch-, nummerserie-, RecId- og
firmatabeller. Agentens database-bruger slettes ikke (den kan være oprettet af grant-least-privilege.sql). Bagefter
viser SQLInsight AX-datatjekkene som "ikke målbar" med den GRANT, der skal til for at slå dem til igen.
Understøtter SQL Server 2005 og nyere. Kør som sysadmin eller som db_owner/db_securityadmin i AX-databasen.
*/
SET NOCOUNT ON;
DECLARE @DatabaseName sysname;
DECLARE @LoginName sysname;
SET @DatabaseName = N'AXDB'; -- <== ret til AX-databasen
SET @LoginName = N'DOMÆNE\svc-sqlinsight$'; -- <== ret til agentens login
IF DB_ID(@DatabaseName) IS NULL
BEGIN
RAISERROR(N'Databasen %s findes ikke.', 16, 1, @DatabaseName);
RETURN;
END;
DECLARE @Procedure nvarchar(300);
DECLARE @Sql nvarchar(max);
SET @Procedure = QUOTENAME(@DatabaseName) + N'.sys.sp_executesql';
SET @Sql = N'
SET NOCOUNT ON;
DECLARE @user sysname;
DECLARE @revoke nvarchar(max);
DECLARE @count int;
SELECT @user = name FROM sys.database_principals WHERE sid = SUSER_SID(@login);
IF @user IS NULL
BEGIN
PRINT N''Login''''et har ingen bruger i '' + DB_NAME() + N''. Intet at fjerne.'';
RETURN;
END;
/* Only the AX schema (where SQLDICTIONARY and DATAAREA are, as grant-dynamics-ax-read.sql finds it), and only a SELECT
that was granted: a DENY, or a grant on a same-named table in another schema set up for another reason, is left alone. */
DECLARE @schema sysname;
SELECT TOP 1 @schema = s.name
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
WHERE UPPER(t.name) = N''SQLDICTIONARY'' AND OBJECT_ID(QUOTENAME(s.name) + N''.DATAAREA'', N''U'') IS NOT NULL
ORDER BY CASE WHEN s.name = N''dbo'' THEN 0 ELSE 1 END;
IF @schema IS NULL
BEGIN
PRINT DB_NAME() + N'' er ikke en Dynamics AX-database (SQLDICTIONARY og DATAAREA findes ikke i samme skema). Intet er ændret.'';
RETURN;
END;
SET @revoke = N'''';
SET @count = 0;
SELECT @revoke = @revoke + N''REVOKE SELECT ON '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) + N'' FROM '' + QUOTENAME(@user) + N''; '', @count = @count + 1
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
WHERE s.name = @schema
AND UPPER(t.name) IN (N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'',
N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''SQLDICTIONARY'', N''DATAAREA'')
AND EXISTS (SELECT 1 FROM sys.database_permissions p
WHERE p.class = 1 AND p.major_id = t.object_id AND p.minor_id = 0 AND p.grantee_principal_id = USER_ID(@user)
AND p.permission_name = N''SELECT'' AND p.state IN (''G'', ''W''));
IF @count > 0 EXEC (@revoke);
PRINT N''SELECT fjernet på '' + CAST(@count AS nvarchar(10)) + N'' AX-tabeller i '' + DB_NAME() + N'' for '' + @user + N''.'';';
EXEC @Procedure @Sql, N'@login sysname', @login = @LoginName;

View file

@ -27,7 +27,7 @@ Kun når `SQLInsight:Hub:Enabled` er `true` og der er en gyldig licens. Agenten
Der sendes aldrig tabeldata, forespørgselsresultater, adgangskoder eller forbindelsesstrenge.
**Dynamics AX** (fra 1.3, udvidet i 1.4): fund fra AX-reglerne (`COM-AX-…` og fra 1.4 `SQLI-AX-…`: batchjob, nummerserier, RecId, AX-brugere bag blokering) og deres resultater sendes som standard ikke (en ændring fra 1.2, der sendte dem; se [opdatering.md](opdatering.md#13-servere-og-databaser)); sendes de, er reglernes årsagstekst erstattet af en fast tekst, og AX-brugernavne er følsomme målinger. Kunden kan slå det til for én AX-database ad gangen under *Servere og databaser* (se [servere-og-databaser.md](servere-og-databaser.md#dynamics-ax-fund-og-sqlinsight-hub)).
**Dynamics AX** (fra 1.3): fund fra AX-reglerne (`COM-AX-…`) og deres resultater sendes som standard ikke (en ændring fra 1.2, der sendte dem; se [opdatering.md](opdatering.md#13-servere-og-databaser)). Kunden kan slå det til for én AX-database ad gangen under *Servere og databaser* (se [servere-og-databaser.md](servere-og-databaser.md#dynamics-ax-fund-og-sqlinsight-hub)).
**Adgangskoder til SQL-logins**, der indtastes i webbrugerfladen, gemmes i kundens repository krypteret med agentens offentlige nøgle, så kun agenten kan læse dem. De forlader aldrig kundens netværk.
@ -67,7 +67,7 @@ Der deles aldrig kundenavne, instans- eller servernavne, databasenavne, målinge
| Instanser, der ikke har rapporteret | 180 dage, med deres fund og versionshistorik |
| Alt om en kunde | Når aftalen ophører, eller kunden beder om det (`delete-customer`) |
Fristerne kan ændres i hubbens konfiguration (se [hub.md](hub.md)). I kundens eget repository styres opbevaringen af `SQLInsight:Retention`, for Windows-servere og syntetiske tjek af `SQLInsight:Servers` og `SQLInsight:SyntheticChecks` (se [windows-servere.md](windows-servere.md#opbevaring)). Fund på Windows-servere og syntetiske tjek sendes ikke til SQLInsight Hub; hændelseslogtekster og fejlbeskeder bliver i kundens repository.
Fristerne kan ændres i hubbens konfiguration (se [hub.md](hub.md)). I kundens eget repository styres opbevaringen af `SQLInsight:Retention`.
## Hvor ligger data?
@ -104,17 +104,3 @@ Deling er slået fra som standard og er ikke en betingelse for Professional elle
Agent og webbrugerflade gemmer data i kundens eget repository hos kunden. Her er kunden selv ansvarlig. Repositoriet indeholder de samme typer data som ovenfor, inklusive følsomme målinger (forespørgselstekst, logins, deadlock-grafer), fordi de er nødvendige for at løse problemerne lokalt. Til [analyse af tidsrum](analyse.md) gemmes desuden de tungeste forespørgsler med tekst og forbrug hvert 5. minut i `RawDays` (standard 14 dage); den tekst bruges kun lokalt og indgår aldrig i det, der sendes til SQLInsight Hub. Adgang til webbrugerfladen styres med Windows-login, og databasebrugeren har kun læserettigheder.
Til [rettighedsrevisionen](rettigheder.md) gemmes navne på personer og konti med forhøjede serverrettigheder (også personer i Windows-grupper, slået op i kundens Active Directory af agenten), hvem der har fået eller mistet dem, og hvem der foretog ændringen. Det er personoplysninger. De bliver i kundens repository: ændringsloggen slettes efter `ResolvedFindingDays` (standard 365 dage), og i fund er navnene følsomme målinger, som sendes til SQLInsight Hub og integrations-API'et som `[udeladt]`. Fundets titel og emne indeholder ingen navne. Revisionsrapporten (PDF) dannes kun lokalt.
Til [Windows-servere](windows-servere.md) og [syntetiske tjek](syntetiske-tjek.md) (fra 1.5) gemmes i kundens repository:
| Data | Kan indeholde | Slettes |
|---|---|---|
| CPU og hukommelse pr. minut | – | efter `SQLInsight:Servers:SampleDays` (standard 14 dage) |
| Ledig plads pr. drev | drevbogstaver og etiketter | efter `SQLInsight:Servers:DiskHistoryDays` (standard 35 dage) |
| Grupperede fejl fra hændelsesloggen med den nyeste tekst som eksempel (højst 500 tegn) | brugernavne, computernavne, stier og IP-adresser | efter `SQLInsight:Servers:EventDays` (standard 90 dage) |
| Tjenester og planlagte opgaver (navne, status, seneste resultat) | navne på tjenestekonti er ikke med; opgaver pr. bruger kan have brugerens SID i navnet | erstattes ved hver indsamling |
| Syntetiske tjeks resultater med besked og detaljer | logins i SQL-fejltekster, adresser | efter `SQLInsight:SyntheticChecks:ResultDays` (standard 14 dage) |
| Et SQL-tjeks login og adgangskode | login'et; adgangskoden kun krypteret til agentens nøgle (som i 1.3) | når tjekket fjernes (login og krypteret adgangskode slettes med det samme) |
| Fund på servere og tjek | følsomme målinger som ovenfor | når de er løst, og derefter efter `ResolvedFindingDays` (standard 365 dage) |
Fundenes titel og emne indeholder ingen hændelsestekster, fejlbeskeder eller logins; de står kun i følsomme målinger, som integrations-API'et viser som `[udeladt]` (også agentens konto, brugernavne og adresser med forespørgselsparametre). Notifikationer (fx Teams og webhooks) får titel og emne. Fund på servere og syntetiske tjek sendes ikke til SQLInsight Hub og indgår endnu ikke i PDF-rapporterne.

View file

@ -1,126 +0,0 @@
# Dynamics AX
Fra version 1.4 overvåger SQLInsight Pro Microsoft Dynamics AX-databaser specifikt: batchjob, nummerserier, RecId og hvilken AX-bruger der står bag en blokering. Det findes under **Dynamics AX** i menuen (vises, så snart en overvåget database er genkendt som AX) og i API'et under `/api/v1/dynamics-ax/`.
> **Hvor sikre er vi på skemaet?** Axapta 3.0 er bygget og testet mod en rigtig kundedatabase ([`schema-ax30.tsv`](dynamics-ax/schema-ax30.tsv): tabeller ejet af `bmssa`, 32-bit RECID, `BATCH` med lokal `STARTDATE` + `STARTTIME` i sekunder efter midnat). **AX 4.0, AX 2009, AX 2012 og Dynamics 365 F&O er baseret på Microsofts dokumenterede skema – ikke verificeret mod kundedata.** Det dokumenterede skema står i [`schema-ax2009.documented.tsv`](dynamics-ax/schema-ax2009.documented.tsv) og [`schema-ax2012.documented.tsv`](dynamics-ax/schema-ax2012.documented.tsv). Leverer en kunde det rigtige skema (se [nedenfor](#nyt-kundeskema)), lægges det ind som `schema-ax2009.tsv`/`schema-ax2012.tsv`, og testene kører så mod det i stedet.
## Genkendelse
En database er AX, når systemtabellerne `SQLDICTIONARY` og `DATAAREA` ligger i samme skema. Versionen afgøres af skemaet ét sted ([`AxDetection.cs`](../src/SQLInsight.Core/DynamicsAx/AxDetection.cs)), som bruges af *Servere og databaser*, AX-collectorerne, community-reglerne `COM-AX-*` og webbrugerfladen:
| Kendetegn | Version |
|---|---|
| `SQLDICTIONARY.RECID` er `int` (præcision 10) | Axapta 2.5/3.0 (kan ikke skelnes; typisk skema `bmssa`) |
| `bigint`, ingen `BATCHJOB` | AX 4.0 |
| `BATCHJOB`, ingen `NUMBERSEQUENCESCOPE` | AX 2009 |
| `NUMBERSEQUENCESCOPE`, ingen `PARTITION`-kolonne | AX 2012 RTM/FP |
| `PARTITION` og modeldatabasen `<db>_model` på instansen | AX 2012 R2/R3 |
| `PARTITION`, intet modellager | Dynamics 365 F&O on-premises (formodet) |
Genkendelsen læser kun metadata (`sys.tables`, `sys.columns`, `HAS_PERMS_BY_NAME`) og kræver ikke mere end [`grant-least-privilege.sql`](../db/permissions/grant-least-privilege.sql). Den kører hver time i de databaser, der er valgt til overvågning (hvert 5. minut for en AX-database, hvor agenten endnu ikke har den fulde læseadgang, så en GRANT virker inden for få minutter). **AX-tjek kører kun i databaser, der er genkendt som AX og valgt til overvågning**; de generelle tjek kører i alle valgte databaser som hidtil.
## Valgfri læseadgang til AX-tabellerne
Batchjob, nummerserier og RecId kræver `SELECT` på nogle få AX-systemtabeller – aldrig kundedata (debitorer, ordrer, posteringer) og aldrig skriverettigheder:
| Version | Tabeller |
|---|---|
| Axapta 3.0 og AX 4.0 | `BATCH`, `NUMBERSEQUENCETABLE`, `NUMBERSEQUENCEREFERENCE`, `SYSTEMSEQUENCES`, `SQLDICTIONARY`, `DATAAREA` |
| AX 2009 | `BATCHJOB`, `BATCHJOBHISTORY`, `NUMBERSEQUENCETABLE`, `NUMBERSEQUENCEREFERENCE`, `SYSTEMSEQUENCES`, `SQLDICTIONARY`, `DATAAREA` |
| AX 2012 og F&O | som AX 2009 plus `NUMBERSEQUENCESCOPE` |
Adgangen er et tilvalg pr. database. **Dynamics AX → AX-databaser** viser for hver database, hvilke tabeller agenten må læse, og de nøjagtige `GRANT`-sætninger med skema (`bmssa` i AX 3.0) og agentens login udfyldt. Det samme for enhver AX-database: [`db/permissions/grant-dynamics-ax-read.sql`](../db/permissions/grant-dynamics-ax-read.sql), som selv finder version og skema (fjernes igen med [`revoke-dynamics-ax-read.sql`](../db/permissions/revoke-dynamics-ax-read.sql)). SQLInsight kører aldrig scripts selv.
Uden adgangen er tjekket **ikke målbart**, og årsagen er den nøjagtige `GRANT`, der mangler – aldrig en fejl.
## Sådan læser SQLInsight AX
- **Kun SELECT**, kun på standardtabellerne ovenfor, og aldrig i kundens database ud over det: al historik (batchkørsler, målinger af nummerserier) gemmes i SQLInsights eget repository.
- **Begrænset:** `TOP` på hver forespørgsel (1000 batchjob; 1000 historikrækker første gang og derefter 3000 fra seneste læste RECID minus et overlap på 2000, så mindst 1000 nye kørsler læses hver gang; de 5000 mest brugte nummerserier; de 500 højeste RecId-serier), og historikken læses efter RECID, som AX altid har et unikt indeks på.
- **Uden at vente på AX:** `READ COMMITTED` med `SET LOCK_TIMEOUT 2000` – er en række låst, opgiver læsningen efter 2 sekunder og prøver igen næste gang (det forrige billede beholdes i op til 15 minutter, så fund ikke blinker). Bagefter sættes agentens egne sessionsindstillinger igen (`LOCK_TIMEOUT 5000`, `READ UNCOMMITTED` til DMV-forespørgslerne), så de næste collectors ikke påvirkes. Har databasen `ALLOW_SNAPSHOT_ISOLATION`, bruges `SNAPSHOT`; med `READ_COMMITTED_SNAPSHOT` læses versioner. **Aldrig `NOLOCK`.**
- **Uden at skifte database:** forespørgslerne køres i AX-databasen med `[db].sys.sp_executesql`.
- **Kolonnetjek først:** hver forespørgsel bygges ud fra de kolonner, databasen faktisk har. Mangler en nødvendig kolonne, er tjekket "ikke målbart" med den præcise kolonne; mangler en valgfri, udelades den med en note.
- **SQL Server 2005 og nyere.** På SQL Server 2000 (legacy ODBC) genkendes AX under *Servere og databaser*, men datatjekkene kører ikke.
Den præcise SQL for hver regel står i [`SQL Scripts/`](../SQL%20Scripts/) (fx `SQLI-AX-003.sql`), genereret fra koden og klar til at køre i SSMS.
## Regler
| Regel | Hvad | Tærskel (`SQLInsight:DynamicsAx`) |
|---|---|---|
| SQLI-AX-001 | Batchjob er endt med fejl (nu, eller seneste kørsel af et gentaget job) | `BatchFailedLookbackHours` = 24 |
| SQLI-AX-002 | Batchjob kører længere end normalt, målt mod jobbets egne seneste kørsler | `BatchLongRunningFactor` = 2 × 90 %-fraktilen, mindst `BatchLongRunningMinimumMinutes` = 10, efter `BatchHistoryMinimumRuns` = 5 kørsler |
| SQLI-AX-003 | Batchjob er ikke startet til tiden; efter et døgn "aldrig startet". Job planlagt for mere end `BatchAbandonedDays` siden regnes som opgivne og meldes ikke | `BatchLateMinutes` = 30, `BatchNeverStartedHours` = 24, `BatchAbandonedDays` = 30 |
| SQLI-AX-004 | Batchjob hænger i Udfører | `BatchStuckHours` = 6 (medmindre jobbet normalt kører så længe) |
| SQLI-AX-005 | Nummerserie er ved at løbe tør, nu eller efter prognosen | `NumberSequenceWarnPercent` = 80, `NumberSequenceCriticalPercent` = 95, `ForecastWarnDays` = 90, `ForecastCriticalDays` = 30 |
| SQLI-AX-006 | Nummerseriens format har færre cifre end Højeste kræver | – |
| SQLI-AX-007 | Spærret nummerserie bruges stadig af en reference | – |
| SQLI-AX-008 | Kontinuerlig nummerserie er ikke ryddet op | 7 dage eller 2 × seriens interval |
| SQLI-AX-009 | RecId er ved at løbe tør – Axapta 3.0's 32-bit RecId advares langt tidligere | `RecId32WarnPercent` = 50, `RecId32CriticalPercent` = 75, `RecId32ForecastDays` = 730; 64-bit: `RecId64WarnPercent` = 80 |
| SQLI-AX-010 | AX-bruger blokerer andre | `BlockingSeconds` = 30 |
Tærsklerne sættes i agentens `appsettings.json`:
```json
"SQLInsight": {
"DynamicsAx": { "BatchLateMinutes": 30, "BatchStuckHours": 6, "NumberSequenceWarnPercent": 80, "ForecastWarnDays": 90 }
}
```
De eksisterende community-regler `COM-AX-001` til `-007` (support, SQL Server-version, MAXDOP, RecId-indeks, heaps, statistikker) læser kun metadata og kører nu også kun i genkendte og valgte AX-databaser.
### Batchjob
Planlagt start er `BATCHJOB.ORIGSTARTDATETIME` (UTC) i AX 2009 og senere, og `BATCH.STARTDATE` + `STARTTIME` (lokal tid, sekunder efter midnat) i Axapta 3.0 og AX 4.0; de lokale tider regnes om til UTC i AX-tidszonen med de sommertidsregler, der gjaldt på datoen. AX-tidszonen er `SQLInsight:DynamicsAx:LocalTimeZone` (fx `Romance Standard Time`; AOS'ens tidszone), ellers agentens egen, når den lige nu har samme forskel til UTC som SQL Server. Passer ingen af dem, bruges SQL Servers aktuelle forskel, og en datanote siger, at tider fra før et sommertidsskift kan være en time forkerte. En tid i timen, der springes over om foråret, eller som kommer to gange om efteråret, regnes med normaltid (om efteråret den sidste af de to), så et job aldrig ser forsinket ud pga. skiftet.
Hvert jobs historik er SQLInsights egen (`insight.AxBatchRuns`, gemt `EventDays` = 90 dage): i AX 2009 og senere fra `BATCHJOBHISTORY`, læst efter RECID; Axapta 3.0 og 4.0 gemmer ingen historik, så SQLInsight registrerer selv de kørsler, den ser blive afsluttet (kørselstiden måles fra første gang, den så jobbet køre, og korte kørsler, der starter og slutter mellem to målinger, kommer ikke med). Et job i Axapta 3.0 er klasse + firma + batchgruppe, fordi AX 3.0 laver en ny række for hver gentagelse.
**`STATUS` i Axapta 3.0** følger samme rækkefølge som AX 4.0 og senere: 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet. Venter (1) og Afsluttet (4) er kontrolleret mod en kundes data med [`dynamics-ax/verificer-ax30-batchstatus.sql`](dynamics-ax/verificer-ax30-batchstatus.sql); 0, 2 og 3 fandtes ikke i dataene. Siden *Batchjob* viser stadig de rå værdier. AX 2009 og senere følger Microsofts dokumenterede `BatchStatus` (0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet, 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret).
### Nummerserier og RecId
Forbrug = (`NEXTREC` − `LOWEST`) i forhold til `HIGHEST` – eller formatets kapacitet (10^antal `#` − 1), hvis den er lavere. Prognoserne bygger på SQLInsights egne målinger: én pr. serie pr. dag af `NEXTREC` og `SYSTEMSEQUENCES.NEXTVAL` (`insight.AxSequenceSamples`, gemt `HourlyDays` = 400 dage). Forbruget pr. dag regnes fra den ældste måling inden for `ForecastBasisDays` (30) dage, der er mindst et døgn gammel; prognosen kommer altså tidligst et døgn efter, at læseadgangen er givet. Manuelle og cykliske serier får ingen prognose.
RecId-grænsen er typens maksimum – `int` (2.147.483.647) når `NEXTVAL` er `int` som i Axapta 3.0, ellers `bigint` – eller `MAXVAL`, hvis den er sat lavere. I Axapta 3.0 er RecId pr. tabel og firma.
### AX-brugere bag blokering
Alle AX-brugere går gennem AOS'ens login. **AX 2009 og 2012** (og F&O) kan skrive AX-brugeren og AX-sessionen i sessionens `CONTEXT_INFO`, når `connectioncontext` er slået til i AOS-konfigurationen (registreringsdatabasen på AOS-serveren; kræver genstart af AOS). SQLInsight læser den fra blokeringsmålingerne (første ord med bogstaver = bruger, første tal = AX-session; formatet er Microsofts for AX 2012 og ikke verificeret mod kundedata). **Axapta 3.0 og AX 4.0 sætter aldrig `CONTEXT_INFO`**: brugeren kan ikke ses fra SQL Server, og SQLInsight siger det og viser vært (AOS'en ved 3-lags-installation, brugerens pc ved 2-lags-klient) og program. AX-brugeren er en følsom oplysning.
## SQLInsight Hub
AX-fund (`SQLI-AX-*` og `COM-AX-*`) sendes kun til SQLInsight Hub for databaser, hvor *AX-fund til SQLInsight Hub* er slået til under [Servere og databaser](servere-og-databaser.md#dynamics-ax-fund-og-sqlinsight-hub) (standard fra). Hvert fund bærer databasen i sit emne (`database: AX30 · batch: …`), så det følger databasens valg. AX-reglernes udfald sendes kun, når mindst én database har valget slået til, og deres årsag (som kan nævne tabeller, logins og GRANT-tekst) erstattes af en fast tekst. AX-brugere er følsomme og sendes kun med `SendSensitiveEvidence`.
## API
| Endpoint | Indhold |
|---|---|
| `GET /api/v1/dynamics-ax/databases` | Genkendte AX-databaser: version, verificeret, skema, modeldatabase, firmaer, læseadgang pr. tabel, manglende tabeller og GRANT |
| `GET /api/v1/dynamics-ax/detection-problems` | Overvågede databaser, AX-genkendelsen ikke kunne læse (fx ingen databasebruger før SQL Server 2014) |
| `GET /api/v1/dynamics-ax/grant-script?instance=&database=` | GRANT-sætningerne og hele `grant-dynamics-ax-read.sql` udfyldt for databasen |
| `GET /api/v1/dynamics-ax/batch-jobs` | Batchjob nu: status, planlagt/faktisk start, forsinkelse, kørselstid, median og 90 %-fraktil, rå STATUS-værdier og hvad de betyder i versionen |
| `GET /api/v1/dynamics-ax/batch-runs?database=&job=&days=7` | Afsluttede kørsler fra repository'et |
| `GET /api/v1/dynamics-ax/number-sequences` | Nummerserier med forbrug, format, prognose og flag |
| `GET /api/v1/dynamics-ax/recids` | RecId pr. tabel med grænse og prognose |
| `GET /api/v1/dynamics-ax/sequence-samples?instance=&database=&kind=N\|R&key=&days=30` | De daglige målinger bag prognoserne (højst 10.000; `X-Truncated: true`, når der er flere) |
| `GET /api/v1/dynamics-ax/blocking?days=7` | AX-brugere bag blokering |
Alle tager `?instance=` for én instans; fjernede servere er ikke med. AX-bruger, vært, program, sætning og rå `CONTEXT_INFO` er `[udeladt]`, og agentens login og databasebruger erstattes af pladsholderen `DOMÆNE\svc-sqlinsight$` (også i GRANT-teksterne i "ikke målbar" og i `/api/v1/instances/{name}/rule-states`), medmindre `SQLInsight:Web:ApiIncludesSensitiveEvidence` er slået til.
## Nyt kundeskema
Får vi et rigtigt skema fra en AX 2009- eller 2012-kunde, eksporteres det med
```sql
SELECT s.name AS skema, t.name AS tabel, c.name AS kolonne, TYPE_NAME(c.system_type_id) AS type, c.max_length, c.column_id
FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id JOIN sys.columns c ON c.object_id = t.object_id
WHERE t.name IN ('SQLDICTIONARY', 'DATAAREA', 'SYSCONFIG', 'BATCH', 'BATCHJOB', 'BATCHJOBHISTORY', 'BATCHGROUP',
'NUMBERSEQUENCETABLE', 'NUMBERSEQUENCESCOPE', 'NUMBERSEQUENCEREFERENCE', 'SYSTEMSEQUENCES')
ORDER BY s.name, t.name, c.column_id;
```
og gemmes tabulatorsepareret som `docs/dynamics-ax/schema-ax2009.tsv` eller `schema-ax2012.tsv`. Testene (`AxSchemaAdapterTests`, `DynamicsAxSqlTests`, `SqlScriptTests`) bruger så det rigtige skema i stedet for det dokumenterede, og viser præcis hvilke kolonner versionens adapter ([`AxSchemaAdapter.cs`](../src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs)) forventer, men som mangler. Al viden om en versions tabeller ligger i dens adapter; ret den dér, og opdatér teksterne "ikke verificeret mod kundedata" for versionen (`AxVersions.IsVerified`).
## Genbrug i andre tjek
Et syntetisk AX-logon-tjek (fase 1.5) hører ikke til her, men kan genbruge AX-viden herfra i stedet for at kende AX' tabeller selv: `GET /api/v1/dynamics-ax/databases` (eller snapshot'et `dynamics-ax` i repository'et, typen `AxDatabaseSnapshot`) giver for hver overvåget AX-database version, skema, firmaer (`DATAAREA`) og modeldatabase; `AxDetection` og `AxSchemaAdapter` i `SQLInsight.Core.DynamicsAx` er den eneste kilde til, hvilke tabeller og kolonner en version har; og `AxSessionMapper` oversætter en SQL-session til AX-bruger og AX-session, hvor AOS'en sætter `CONTEXT_INFO`.

View file

@ -1,129 +0,0 @@
skema tabel kolonne type max_length column_id
# Baseret på Microsofts dokumenterede skema – ikke verificeret mod kundedata. Erstattes af schema-ax2009.tsv, når en kunde leverer det rigtige skema.
dbo SQLDICTIONARY TABLEID int 4 1
dbo SQLDICTIONARY FIELDID int 4 2
dbo SQLDICTIONARY ARRAY int 4 3
dbo SQLDICTIONARY NAME nvarchar 80 4
dbo SQLDICTIONARY SQLNAME nvarchar 80 5
dbo SQLDICTIONARY FIELDTYPE int 4 6
dbo SQLDICTIONARY STRSIZE int 4 7
dbo SQLDICTIONARY SHADOW int 4 8
dbo SQLDICTIONARY RIGHTJUSTIFY int 4 9
dbo SQLDICTIONARY NULLABLE int 4 10
dbo SQLDICTIONARY FLAGS int 4 11
dbo SQLDICTIONARY RECVERSION int 4 12
dbo SQLDICTIONARY RECID bigint 8 13
dbo DATAAREA ID nvarchar 8 1
dbo DATAAREA NAME nvarchar 120 2
dbo DATAAREA ISVIRTUAL int 4 3
dbo DATAAREA ALWAYSNATIVE int 4 4
dbo DATAAREA TIMEZONE int 4 5
dbo DATAAREA RECVERSION int 4 6
dbo DATAAREA RECID bigint 8 7
dbo SYSCONFIG CONFIGTYPE int 4 1
dbo SYSCONFIG ID int 4 2
dbo SYSCONFIG VALUE nvarchar 160 3
dbo SYSCONFIG MODIFIEDDATETIME datetime 8 4
dbo SYSCONFIG MODIFIEDBY nvarchar 10 5
dbo SYSCONFIG CREATEDDATETIME datetime 8 6
dbo SYSCONFIG CREATEDBY nvarchar 10 7
dbo SYSCONFIG RECVERSION int 4 8
dbo SYSCONFIG RECID bigint 8 9
dbo BATCHJOB CAPTION nvarchar 200 1
dbo BATCHJOB STATUS int 4 2
dbo BATCHJOB ORIGSTARTDATETIME datetime 8 3
dbo BATCHJOB ORIGSTARTDATETIMETZID int 4 4
dbo BATCHJOB STARTDATETIME datetime 8 5
dbo BATCHJOB ENDDATETIME datetime 8 6
dbo BATCHJOB RECURRENCEDATA image 16 7
dbo BATCHJOB ALERTSPROCESSED int 4 8
dbo BATCHJOB CANCELEDBY nvarchar 10 9
dbo BATCHJOB COMPANY nvarchar 8 10
dbo BATCHJOB LOGLEVEL int 4 11
dbo BATCHJOB BATCHCREATEDBY nvarchar 10 12
dbo BATCHJOB MODIFIEDDATETIME datetime 8 13
dbo BATCHJOB MODIFIEDBY nvarchar 10 14
dbo BATCHJOB CREATEDDATETIME datetime 8 15
dbo BATCHJOB CREATEDBY nvarchar 10 16
dbo BATCHJOB RECVERSION int 4 17
dbo BATCHJOB RECID bigint 8 18
dbo BATCHJOBHISTORY BATCHJOBID bigint 8 1
dbo BATCHJOBHISTORY CAPTION nvarchar 200 2
dbo BATCHJOBHISTORY STATUS int 4 3
dbo BATCHJOBHISTORY ORIGSTARTDATETIME datetime 8 4
dbo BATCHJOBHISTORY STARTDATETIME datetime 8 5
dbo BATCHJOBHISTORY ENDDATETIME datetime 8 6
dbo BATCHJOBHISTORY COMPANY nvarchar 8 7
dbo BATCHJOBHISTORY CANCELEDBY nvarchar 10 8
dbo BATCHJOBHISTORY BATCHCREATEDBY nvarchar 10 9
dbo BATCHJOBHISTORY RECVERSION int 4 10
dbo BATCHJOBHISTORY RECID bigint 8 11
dbo BATCH BATCHJOBID bigint 8 1
dbo BATCH GROUPID nvarchar 20 2
dbo BATCH STATUS int 4 3
dbo BATCH CLASSNUMBER int 4 4
dbo BATCH CAPTION nvarchar 200 5
dbo BATCH STARTDATETIME datetime 8 6
dbo BATCH ENDDATETIME datetime 8 7
dbo BATCH SERVERID nvarchar 40 8
dbo BATCH COMPANY nvarchar 8 9
dbo BATCH PARAMETERS image 16 10
dbo BATCH INFO image 16 11
dbo BATCH SESSIONIDX int 4 12
dbo BATCH SESSIONLOGINDATETIME datetime 8 13
dbo BATCH RECVERSION int 4 14
dbo BATCH RECID bigint 8 15
dbo BATCHGROUP GROUP_ nvarchar 20 1
dbo BATCHGROUP DESCRIPTION nvarchar 120 2
dbo BATCHGROUP RECVERSION int 4 3
dbo BATCHGROUP RECID bigint 8 4
dbo NUMBERSEQUENCETABLE NUMBERSEQUENCE nvarchar 40 1
dbo NUMBERSEQUENCETABLE TXT nvarchar 120 2
dbo NUMBERSEQUENCETABLE LOWEST int 4 3
dbo NUMBERSEQUENCETABLE HIGHEST int 4 4
dbo NUMBERSEQUENCETABLE NEXTREC int 4 5
dbo NUMBERSEQUENCETABLE BLOCKED int 4 6
dbo NUMBERSEQUENCETABLE FORMAT nvarchar 40 7
dbo NUMBERSEQUENCETABLE CONTINUOUS int 4 8
dbo NUMBERSEQUENCETABLE CYCLIC int 4 9
dbo NUMBERSEQUENCETABLE CLEANATACCESS int 4 10
dbo NUMBERSEQUENCETABLE INUSE int 4 11
dbo NUMBERSEQUENCETABLE NOINCREMENT int 4 12
dbo NUMBERSEQUENCETABLE CLEANINTERVAL numeric 13 13
dbo NUMBERSEQUENCETABLE LATESTCLEANDATETIME datetime 8 14
dbo NUMBERSEQUENCETABLE ALLOWCHANGEUP int 4 15
dbo NUMBERSEQUENCETABLE ALLOWCHANGEDOWN int 4 16
dbo NUMBERSEQUENCETABLE MANUAL int 4 17
dbo NUMBERSEQUENCETABLE FETCHAHEADQTY int 4 18
dbo NUMBERSEQUENCETABLE FETCHAHEAD int 4 19
dbo NUMBERSEQUENCETABLE DATAAREAID nvarchar 8 20
dbo NUMBERSEQUENCETABLE RECVERSION int 4 21
dbo NUMBERSEQUENCETABLE RECID bigint 8 22
dbo NUMBERSEQUENCEREFERENCE NUMBERSEQUENCE nvarchar 40 1
dbo NUMBERSEQUENCEREFERENCE DATATYPEID int 4 2
dbo NUMBERSEQUENCEREFERENCE CONFIGURATIONKEYID int 4 3
dbo NUMBERSEQUENCEREFERENCE MODULE int 4 4
dbo NUMBERSEQUENCEREFERENCE REFERENCEHELP nvarchar 510 5
dbo NUMBERSEQUENCEREFERENCE REFERENCELABEL nvarchar 120 6
dbo NUMBERSEQUENCEREFERENCE DATATYPESAMEASID int 4 7
dbo NUMBERSEQUENCEREFERENCE WIZARDCONTINUOUS int 4 8
dbo NUMBERSEQUENCEREFERENCE ALLOWSAMEAS int 4 9
dbo NUMBERSEQUENCEREFERENCE SORTFIELD int 4 10
dbo NUMBERSEQUENCEREFERENCE GROUPENABLED int 4 11
dbo NUMBERSEQUENCEREFERENCE WIZARDHIGHEST int 4 12
dbo NUMBERSEQUENCEREFERENCE WIZARDLOWEST int 4 13
dbo NUMBERSEQUENCEREFERENCE WIZARDMANUAL int 4 14
dbo NUMBERSEQUENCEREFERENCE WIZARDFETCHAHEADQTY int 4 15
dbo NUMBERSEQUENCEREFERENCE DATAAREAID nvarchar 8 16
dbo NUMBERSEQUENCEREFERENCE RECVERSION int 4 17
dbo NUMBERSEQUENCEREFERENCE RECID bigint 8 18
dbo SYSTEMSEQUENCES ID int 4 1
dbo SYSTEMSEQUENCES NEXTVAL bigint 8 2
dbo SYSTEMSEQUENCES MINVAL bigint 8 3
dbo SYSTEMSEQUENCES MAXVAL bigint 8 4
dbo SYSTEMSEQUENCES CYCLE int 4 5
dbo SYSTEMSEQUENCES NAME nvarchar 40 6
dbo SYSTEMSEQUENCES TABID int 4 7
dbo SYSTEMSEQUENCES DATAAREAID nvarchar 8 8
dbo SYSTEMSEQUENCES RECVERSION int 4 9
dbo SYSTEMSEQUENCES RECID bigint 8 10
Can't render this file because it has a wrong number of fields in line 2.

View file

@ -1,135 +0,0 @@
skema tabel kolonne type max_length column_id
# Baseret på Microsofts dokumenterede skema – ikke verificeret mod kundedata. Erstattes af schema-ax2012.tsv, når en kunde leverer det rigtige skema.
dbo SQLDICTIONARY TABLEID int 4 1
dbo SQLDICTIONARY FIELDID int 4 2
dbo SQLDICTIONARY ARRAY int 4 3
dbo SQLDICTIONARY NAME nvarchar 80 4
dbo SQLDICTIONARY SQLNAME nvarchar 80 5
dbo SQLDICTIONARY FIELDTYPE int 4 6
dbo SQLDICTIONARY STRSIZE int 4 7
dbo SQLDICTIONARY SHADOW int 4 8
dbo SQLDICTIONARY RIGHTJUSTIFY int 4 9
dbo SQLDICTIONARY NULLABLE int 4 10
dbo SQLDICTIONARY FLAGS int 4 11
dbo SQLDICTIONARY RECVERSION int 4 12
dbo SQLDICTIONARY RECID bigint 8 13
dbo DATAAREA ID nvarchar 8 1
dbo DATAAREA NAME nvarchar 120 2
dbo DATAAREA ISVIRTUAL int 4 3
dbo DATAAREA ALWAYSNATIVE int 4 4
dbo DATAAREA TIMEZONE int 4 5
dbo DATAAREA RECVERSION int 4 6
dbo DATAAREA PARTITION bigint 8 7
dbo DATAAREA RECID bigint 8 8
dbo SYSCONFIG CONFIGTYPE int 4 1
dbo SYSCONFIG ID int 4 2
dbo SYSCONFIG VALUE nvarchar 160 3
dbo SYSCONFIG MODIFIEDDATETIME datetime 8 4
dbo SYSCONFIG MODIFIEDBY nvarchar 16 5
dbo SYSCONFIG CREATEDDATETIME datetime 8 6
dbo SYSCONFIG CREATEDBY nvarchar 16 7
dbo SYSCONFIG RECVERSION int 4 8
dbo SYSCONFIG PARTITION bigint 8 9
dbo SYSCONFIG RECID bigint 8 10
dbo BATCHJOB CAPTION nvarchar 200 1
dbo BATCHJOB STATUS int 4 2
dbo BATCHJOB ORIGSTARTDATETIME datetime 8 3
dbo BATCHJOB ORIGSTARTDATETIMETZID int 4 4
dbo BATCHJOB STARTDATETIME datetime 8 5
dbo BATCHJOB ENDDATETIME datetime 8 6
dbo BATCHJOB RECURRENCEDATA image 16 7
dbo BATCHJOB ALERTSPROCESSED int 4 8
dbo BATCHJOB CANCELEDBY nvarchar 16 9
dbo BATCHJOB COMPANY nvarchar 8 10
dbo BATCHJOB LOGLEVEL int 4 11
dbo BATCHJOB BATCHCREATEDBY nvarchar 16 12
dbo BATCHJOB RUNTIMEJOB int 4 13
dbo BATCHJOB MODIFIEDDATETIME datetime 8 14
dbo BATCHJOB MODIFIEDBY nvarchar 16 15
dbo BATCHJOB CREATEDDATETIME datetime 8 16
dbo BATCHJOB CREATEDBY nvarchar 16 17
dbo BATCHJOB RECVERSION int 4 18
dbo BATCHJOB PARTITION bigint 8 19
dbo BATCHJOB RECID bigint 8 20
dbo BATCHJOBHISTORY BATCHJOBID bigint 8 1
dbo BATCHJOBHISTORY CAPTION nvarchar 200 2
dbo BATCHJOBHISTORY STATUS int 4 3
dbo BATCHJOBHISTORY ORIGSTARTDATETIME datetime 8 4
dbo BATCHJOBHISTORY STARTDATETIME datetime 8 5
dbo BATCHJOBHISTORY ENDDATETIME datetime 8 6
dbo BATCHJOBHISTORY COMPANY nvarchar 8 7
dbo BATCHJOBHISTORY CANCELEDBY nvarchar 16 8
dbo BATCHJOBHISTORY BATCHCREATEDBY nvarchar 16 9
dbo BATCHJOBHISTORY RECVERSION int 4 10
dbo BATCHJOBHISTORY PARTITION bigint 8 11
dbo BATCHJOBHISTORY RECID bigint 8 12
dbo BATCH BATCHJOBID bigint 8 1
dbo BATCH GROUPID nvarchar 20 2
dbo BATCH STATUS int 4 3
dbo BATCH CLASSNUMBER int 4 4
dbo BATCH CAPTION nvarchar 200 5
dbo BATCH STARTDATETIME datetime 8 6
dbo BATCH ENDDATETIME datetime 8 7
dbo BATCH SERVERID nvarchar 40 8
dbo BATCH COMPANY nvarchar 8 9
dbo BATCH PARAMETERS image 16 10
dbo BATCH INFO image 16 11
dbo BATCH SESSIONIDX int 4 12
dbo BATCH SESSIONLOGINDATETIME datetime 8 13
dbo BATCH RETRYCOUNT int 4 14
dbo BATCH RECVERSION int 4 15
dbo BATCH PARTITION bigint 8 16
dbo BATCH RECID bigint 8 17
dbo BATCHGROUP GROUP_ nvarchar 20 1
dbo BATCHGROUP DESCRIPTION nvarchar 120 2
dbo BATCHGROUP RECVERSION int 4 3
dbo BATCHGROUP PARTITION bigint 8 4
dbo BATCHGROUP RECID bigint 8 5
dbo NUMBERSEQUENCETABLE NUMBERSEQUENCE nvarchar 40 1
dbo NUMBERSEQUENCETABLE TXT nvarchar 120 2
dbo NUMBERSEQUENCETABLE LOWEST bigint 8 3
dbo NUMBERSEQUENCETABLE HIGHEST bigint 8 4
dbo NUMBERSEQUENCETABLE NEXTREC bigint 8 5
dbo NUMBERSEQUENCETABLE BLOCKED int 4 6
dbo NUMBERSEQUENCETABLE FORMAT nvarchar 80 7
dbo NUMBERSEQUENCETABLE ANNOTATEDFORMAT nvarchar 80 8
dbo NUMBERSEQUENCETABLE CONTINUOUS int 4 9
dbo NUMBERSEQUENCETABLE CYCLIC int 4 10
dbo NUMBERSEQUENCETABLE CLEANATACCESS int 4 11
dbo NUMBERSEQUENCETABLE INUSE int 4 12
dbo NUMBERSEQUENCETABLE NOINCREMENT int 4 13
dbo NUMBERSEQUENCETABLE CLEANINTERVAL numeric 13 14
dbo NUMBERSEQUENCETABLE LATESTCLEANDATETIME datetime 8 15
dbo NUMBERSEQUENCETABLE ALLOWCHANGEUP int 4 16
dbo NUMBERSEQUENCETABLE ALLOWCHANGEDOWN int 4 17
dbo NUMBERSEQUENCETABLE MANUAL int 4 18
dbo NUMBERSEQUENCETABLE FETCHAHEADQTY int 4 19
dbo NUMBERSEQUENCETABLE FETCHAHEAD int 4 20
dbo NUMBERSEQUENCETABLE NUMBERSEQUENCESCOPE bigint 8 21
dbo NUMBERSEQUENCETABLE RECVERSION int 4 22
dbo NUMBERSEQUENCETABLE PARTITION bigint 8 23
dbo NUMBERSEQUENCETABLE RECID bigint 8 24
dbo NUMBERSEQUENCESCOPE DATAAREA nvarchar 8 1
dbo NUMBERSEQUENCESCOPE LEGALENTITY bigint 8 2
dbo NUMBERSEQUENCESCOPE OPERATINGUNIT bigint 8 3
dbo NUMBERSEQUENCESCOPE OPERATINGUNITTYPE int 4 4
dbo NUMBERSEQUENCESCOPE FISCALCALENDARPERIOD bigint 8 5
dbo NUMBERSEQUENCESCOPE RECVERSION int 4 6
dbo NUMBERSEQUENCESCOPE PARTITION bigint 8 7
dbo NUMBERSEQUENCESCOPE RECID bigint 8 8
dbo NUMBERSEQUENCEREFERENCE NUMBERSEQUENCEDATATYPE bigint 8 1
dbo NUMBERSEQUENCEREFERENCE NUMBERSEQUENCEID bigint 8 2
dbo NUMBERSEQUENCEREFERENCE NUMBERSEQUENCESCOPE bigint 8 3
dbo NUMBERSEQUENCEREFERENCE RECVERSION int 4 4
dbo NUMBERSEQUENCEREFERENCE PARTITION bigint 8 5
dbo NUMBERSEQUENCEREFERENCE RECID bigint 8 6
dbo SYSTEMSEQUENCES ID int 4 1
dbo SYSTEMSEQUENCES NEXTVAL bigint 8 2
dbo SYSTEMSEQUENCES MINVAL bigint 8 3
dbo SYSTEMSEQUENCES MAXVAL bigint 8 4
dbo SYSTEMSEQUENCES CYCLE int 4 5
dbo SYSTEMSEQUENCES NAME nvarchar 40 6
dbo SYSTEMSEQUENCES TABID int 4 7
dbo SYSTEMSEQUENCES DATAAREAID nvarchar 8 8
dbo SYSTEMSEQUENCES RECVERSION int 4 9
dbo SYSTEMSEQUENCES RECID bigint 8 10
Can't render this file because it has a wrong number of fields in line 2.

View file

@ -1,53 +0,0 @@
-- ===================================================================================================================
-- Verificér STATUS-værdierne i BATCH for Axapta 3.0
-- ===================================================================================================================
-- Formål: SQLInsight bruger for AX 3.0: 0 = Tilbageholdt, 1 = Venter, 2 = Udfører, 3 = Fejl, 4 = Afsluttet
-- (1 og 4 kontrolleret hos Cascorp 2026-10-03).
-- Tekst-labels for enums ligger ikke i databasen, så scriptet viser værdierne sammen med de
-- kolonner, der afslører betydningen (sluttid, fejlkode, planlagt start), og eksempler, som kan
-- slås op i AX-klienten (Basis > Forespørgsler > Batchliste).
-- Read-only: Kun SELECT. Ingen temp-tabeller, intet skrives. Kort lock timeout, så batchkørsler ikke blokeres.
-- Kør: I SSMS i AX-databasen (fx AXDBTEST på KGA-SQL-01\AX30). Skemaet er bmssa hos Cascorp;
-- hedder det dbo hos jer, så erstat "bmssa." med "dbo.".
-- ===================================================================================================================
SET NOCOUNT ON;
SET LOCK_TIMEOUT 2000;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 1. Fordeling: hvor mange job har hver STATUS, og hvad kendetegner dem?
SELECT
b.STATUS,
COUNT(*) AS antal,
SUM(CASE WHEN b.ENDDATE > '19000101' THEN 1 ELSE 0 END) AS med_sluttid,
SUM(CASE WHEN b.EXCEPTIONCODE <> 0 THEN 1 ELSE 0 END) AS med_fejlkode,
SUM(CASE WHEN DATEADD(second, b.STARTTIME, b.STARTDATE) > GETDATE()
THEN 1 ELSE 0 END) AS start_i_fremtiden,
SUM(CASE WHEN b.SESSIONIDX <> 0 THEN 1 ELSE 0 END) AS med_session,
MIN(DATEADD(second, b.STARTTIME, b.STARTDATE)) AS tidligste_start,
MAX(DATEADD(second, b.STARTTIME, b.STARTDATE)) AS seneste_start
FROM bmssa.BATCH AS b
GROUP BY b.STATUS
ORDER BY b.STATUS;
-- 2. Op til 5 eksempler pr. STATUS (nyeste først) til at slå op i AX-klientens batchliste.
SELECT x.STATUS, x.COMPANY, x.GROUPID, x.CLASSNUM, x.JOURNALID, x.EXECUTEDBY,
x.planlagt_start, x.slut, x.EXCEPTIONCODE, x.SESSIONIDX, x.RECID
FROM (
SELECT b.STATUS, b.COMPANY, b.GROUPID, b.CLASSNUM, b.JOURNALID, b.EXECUTEDBY,
DATEADD(second, b.STARTTIME, b.STARTDATE) AS planlagt_start,
CASE WHEN b.ENDDATE > '19000101'
THEN DATEADD(second, b.ENDTIME, b.ENDDATE) END AS slut,
b.EXCEPTIONCODE, b.SESSIONIDX, b.RECID,
ROW_NUMBER() OVER (PARTITION BY b.STATUS ORDER BY b.STARTDATE DESC, b.STARTTIME DESC) AS rn
FROM bmssa.BATCH AS b
) AS x
WHERE x.rn <= 5
ORDER BY x.STATUS, x.planlagt_start DESC;
-- 3. Hvilken feltype har STATUS ifølge AX' egen dictionary? (FIELDTYPE 4 = enum)
SELECT d.TABLEID, d.FIELDID, d.NAME, d.SQLNAME, d.FIELDTYPE
FROM bmssa.SQLDICTIONARY AS d
WHERE d.SQLNAME = 'STATUS'
AND d.TABLEID = (SELECT TOP (1) t.TABLEID FROM bmssa.SQLDICTIONARY AS t
WHERE t.FIELDID = 0 AND t.SQLNAME = 'BATCH');

View file

@ -90,10 +90,6 @@ Agenten opretter tabellerne selv ved første start.
Kør [`db/permissions/grant-least-privilege.sql`](../db/permissions/grant-least-privilege.sql) på hver instans efter at have oprettet agentens login. Agenten bliver aldrig sysadmin.
Overvåges Dynamics AX, kan agenten desuden få læseadgang til nogle få AX-systemtabeller (batchjob, nummerserier, RecId) med [`db/permissions/grant-dynamics-ax-read.sql`](../db/permissions/grant-dynamics-ax-read.sql). Det er et tilvalg pr. database; se [dynamics-ax.md](dynamics-ax.md).
Windows-serverne (CPU, diske, tjenester, hændelseslog, opgaver) læses med [`db/permissions/grant-windows-server.ps1`](../db/permissions/grant-windows-server.ps1), som giver agentens konto læseadgang uden lokal administrator; se [windows-servere.md](windows-servere.md#rettigheder-aldrig-lokal-administrator).
## 4. Agenten som Windows-tjeneste
```powershell
@ -188,4 +184,4 @@ Se [opdatering.md](opdatering.md): regelpakken opdateres automatisk, og nye vers
## Menuen i webbrugerfladen
Menuen er delt i grupper, der kan foldes ud og sammen: **Overvågning** (med *Servere og databaser*, se [servere-og-databaser.md](servere-og-databaser.md)), **Servere** (*Windows-servere* og *Syntetiske tjek*, se [windows-servere.md](windows-servere.md) og [syntetiske-tjek.md](syntetiske-tjek.md)), **Analyse**, **Konfiguration og sikkerhed**, **Rapporter**, **Dynamics AX** (*AX-databaser*, *Batchjob*, *Nummerserier*, *RecId* og *AX-brugere bag blokering*; kun når en database er genkendt som AX, se [dynamics-ax.md](dynamics-ax.md)), **Support**, **System** og **Community**. Gruppen med den side, man står på, er åben. Sider, der findes pr. instans (fx Analyse af tidsrum og Konfiguration), åbner et valg af instans; er der kun én instans, går de direkte til den. På en smal skærm (mobil) folder hele menuen sig sammen bag knappen **Menu**. Alt virker uden JavaScript.
Menuen er delt i grupper, der kan foldes ud og sammen: **Overvågning** (med *Servere og databaser*, se [servere-og-databaser.md](servere-og-databaser.md)), **Analyse**, **Konfiguration og sikkerhed**, **Rapporter**, **Dynamics AX** (kun når en database er genkendt som AX), **Support**, **System** og **Community**. Gruppen med den side, man står på, er åben. Sider, der findes pr. instans (fx Analyse af tidsrum og Konfiguration), åbner et valg af instans; er der kun én instans, går de direkte til den. På en smal skærm (mobil) folder hele menuen sig sammen bag knappen **Menu**. Alt virker uden JavaScript.

View file

@ -89,13 +89,10 @@ Alt, hvad webbrugerfladen viser, kan hentes som JSON under `/api/v1/` med de sam
| `GET /api/v1/instances/{navn}/replication` | Replikering set fra distributøren: hver distributionsagents status, seneste kørsel og forsinkelse, og fejl det seneste døgn |
| `GET /api/v1/instances/{navn}/index-usage?days=30` | Læsninger og skrivninger pr. index summeret over dagene (1–400), på tværs af genstarter, mindst læste først |
| `POST /api/v1/support-cases`, `GET /api/v1/support-cases[/{id}]`, `POST /api/v1/support-cases/{id}/close` | Supportsager: se [support.md](support.md) (kræver scope `support:write` for at skrive) |
| `GET /api/v1/dynamics-ax/databases`, `…/grant-script`, `…/batch-jobs`, `…/batch-runs`, `…/number-sequences`, `…/recids`, `…/sequence-samples`, `…/blocking` | Dynamics AX: genkendte AX-databaser med læseadgang og GRANT, batchjob og kørsler, nummerserier, RecId, målingerne bag prognoserne og AX-brugere bag blokering. AX-brugere og logins er `[udeladt]`; se [dynamics-ax.md](dynamics-ax.md#api) |
| `GET`/`POST /api/v1/monitored-instances`, `…/{id}`, `…/{id}/connection-test`, `…/{id}/discovery`, `…/{id}/databases`, `…/{id}/grant-script`, `POST /api/v1/instance-discovery`, `GET /api/v1/agent-requests/{id}` | Hvilke servere og databaser der overvåges, forbindelsestest, søgning og GRANT-script: se [servere-og-databaser.md](servere-og-databaser.md#api) (kræver scope `config:write` for at ændre) |
| `GET`/`POST /api/v1/windows-servers`, `GET`/`PATCH`/`DELETE …/{id}`, `…/{id}/test`, `…/{id}/grant-script`, `…/{id}/performance`, `…/{id}/disks`, `…/{id}/services`, `…/{id}/events`, `…/{id}/tasks`, `…/{id}/rule-states`, `GET /api/v1/server-findings[/{id}]` | Windows-servere: CPU, hukommelse, diske med prognose, tjenester, hændelseslog, planlagte opgaver og deres fund: se [windows-servere.md](windows-servere.md#api) (kræver scope `config:write` for at ændre) |
| `GET`/`POST /api/v1/synthetic-checks`, `GET`/`PUT`/`PATCH`/`DELETE …/{id}`, `…/{id}/run`, `…/{id}/results`, `…/{id}/rule-states` | Syntetiske tjek: se [syntetiske-tjek.md](syntetiske-tjek.md#api) (kræver scope `config:write` for at ændre) |
| `GET /metrics` | Prometheus-format |
**Følsomme data:** forespørgselstekster, logins, personnavne, værts- og programnavne og tjenestekonti vises som `[udeladt]`, medmindre `SQLInsight:Web:ApiIncludesSensitiveEvidence` er `true`. Det gælder både fund, rapporter i JSON og sidernes data, fra 1.5 også agentens konto i fejltekster, hændelseslogtekster, syntetiske tjeks fejlbeskeder og SQL-logins og forespørgselsparametre i adresser. PDF-rapporterne indeholder de samme data som i webbrugerfladen.
**Følsomme data:** forespørgselstekster, logins, personnavne, værts- og programnavne og tjenestekonti vises som `[udeladt]`, medmindre `SQLInsight:Web:ApiIncludesSensitiveEvidence` er `true`. Det gælder både fund, rapporter i JSON og sidernes data. PDF-rapporterne indeholder de samme data som i webbrugerfladen.
**Tokens og scopes:** et token i den gamle form (`"navn": "sha256:…"`) må kun læse (`read`). Et token med flere rettigheder skrives som

View file

@ -13,32 +13,6 @@ Begge kommer fra et **opdateringsfeed**: en https-adresse eller en mappe/et fild
Læs dette, før en ny version installeres. Repository-skemaet opgraderes af agenten ved første start.
### 1.5.2: Axapta 3.0-batchstatus rettet
- Batchjobbenes `STATUS` i Axapta 3.0 tolkes nu som i AX 4.0 og senere: 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet (kontrolleret mod en kundes data). Før 1.5.2 blev afsluttede job vist som tilbageholdte og ventende som kørende, så batchfund og -sider for AX 3.0 var forkerte. Fund fra den gamle tolkning lukkes ved næste måling.
- SQLI-AX-003 melder ikke længere ventende job, der var planlagt for mere end `SQLInsight:DynamicsAx:BatchAbandonedDays` (30) dage siden: de er opgivne rækker, ikke en aktuel fejl.
- Ingen skemaændringer.
### 1.5: Windows-servere og syntetiske tjek
- **Ny menu – Servere:** *Windows-servere* (CPU, hukommelse, diske med prognose for, hvornår de er fulde, tjenester, hændelseslog og planlagte opgaver) og *Syntetiske tjek* (TCP-port, HTTP(S) med certifikatets udløb, SQL-login og forespørgsel, DNS og Dynamics AX-logon), og det samme i API v1 under `/api/v1/windows-servers`, `/api/v1/synthetic-checks` og `/api/v1/server-findings` (ændringer kræver scope `config:write`, som i 1.3). Nye regler `SQLI-WIN-001` … `SQLI-WIN-013` og `SQLI-SYN-001` … `SQLI-SYN-003`. Se [windows-servere.md](windows-servere.md) og [syntetiske-tjek.md](syntetiske-tjek.md).
- **SQL Server-hosts kommer med af sig selv** som Windows-servere med rollen *SQL Server* (slås fra med `SQLInsight:Servers:IncludeSqlHosts`). Uden de nye rettigheder er deres områder blot *ikke målbare* med årsagen; SQL Server-overvågningen er uændret.
- **Nye rettigheder pr. server:** agentens konto skal have læseadgang på hver Windows-server, der skal overvåges. Den gives med [db/permissions/grant-windows-server.ps1](../db/permissions/grant-windows-server.ps1), som webbrugerfladen viser udfyldt med agentens konto under serveren. Aldrig lokal administrator og aldrig ret til at køre kode: Performance Monitor Users, Event Log Readers, Distributed COM Users, WMI *Enable Account* + *Remote Enable* på `root\cimv2` og `root\Microsoft\Windows\TaskScheduler`, læseret på Service Control Manager og, kun med `-WinRM`, læseret i WinRM. Scriptet ændrer sikkerheden på kundens server: **afprøv det først på en server, der ikke er i produktion**, og kør det med `-WhatIf` for at se planen. Det gemmer de oprindelige indstillinger, før det ændrer noget, og `-Remove` fjerner præcis det, det selv har givet. SQLInsight kører aldrig selv scriptet.
- **Firewall** fra agentens server til de overvågede servere: TCP 135 og dynamiske RPC-porte (WMI over DCOM og hændelsesloggen) og, med `-WinRM`, TCP 5985. `grant-windows-server.ps1 -EnableFirewall -AgentAddress <agentens IP>` opretter regler kun for agentens adresse; Windows' egne regler røres ikke. Syntetiske tjek bruger de porte, de tjekker (fx 2712, 443, 1433, 53).
- **Kun læsning:** agenten læser med CIM-forespørgsler, hændelsesloggen og Task Schedulers skrivebeskyttede `GetInfoByName`; den starter, stopper eller ændrer intet på serverne. Et syntetisk SQL-tjek kører én skrivebeskyttet `SELECT` i en transaktion, der altid rulles tilbage (fra 1.5 tjekkes også egne regler og community-regler strengere: ingen ekstra sætninger uden semikolon, sekvenser eller låse-hints).
- **Ikke med endnu:** fund på servere og syntetiske tjek sendes **ikke** til SQLInsight Hub og indgår **ikke** i PDF-rapporterne. De vises i webbrugerfladen, i API v1 og som notifikationer.
- **Repository:** migration 0018 (Windows-servere, målinger, hændelser, syntetiske tjek og deres fund) køres af agenten ved første start; data ryddes op efter `SQLInsight:Servers` og `SQLInsight:SyntheticChecks` (se [windows-servere.md](windows-servere.md#opbevaring)).
### 1.4: Dynamics AX
- **Ny menu – Dynamics AX:** vises, så snart en overvåget database er genkendt som Dynamics AX, med siderne AX-databaser, Batchjob, Nummerserier, RecId og AX-brugere bag blokering, og de samme data i API v1 under `/api/v1/dynamics-ax/…` (scope `read`). Nye regler `SQLI-AX-001` … `SQLI-AX-010`. Se [dynamics-ax.md](dynamics-ax.md).
- **Ny, valgfri læseadgang:** batchjob, nummerserier og RecId kræver `SELECT` på nogle få af AX' egne systemtabeller (aldrig kundedata, aldrig mere end `SELECT`). Den gives pr. AX-database med [db/permissions/grant-dynamics-ax-read.sql](../db/permissions/grant-dynamics-ax-read.sql) (og fjernes igen med `revoke-dynamics-ax-read.sql`); den nøjagtige GRANT står under **Dynamics AX → AX-databaser**. SQLInsight kører aldrig selv scriptet.
- **Uden den GRANT ændres intet:** agenten læser ingen AX-tabeller, de nye AX-regler er *ikke målbare* med den GRANT, der skal til, og alt andet virker som i 1.3. Genkendelsen af AX og AX-brugere bag blokering bruger kun de rettigheder, `grant-least-privilege.sql` allerede giver. Læsningerne er altid `SELECT` med `TOP`, med 2 sekunders lock timeout (eller snapshot), aldrig `NOLOCK`, og kun i databaser, der er genkendt som AX **og** valgt til overvågning.
- **AX-fund til SQLInsight Hub** følger stadig tilvalget pr. database fra 1.3 (standard fra) og gælder nu også `SQLI-AX-…`.
- **Axapta 3.0/4.0 og tidszone:** de lokale batchtider omregnes med AOS'ens tidszone. Kører agenten i en anden tidszone end AOS'en, så sæt `SQLInsight:DynamicsAx:LocalTimeZone` (fx `Romance Standard Time`).
- **AX-versioner:** kun Axapta 3.0 er verificeret mod en kundes database. AX 4.0, AX 2009, AX 2012 og Dynamics 365 F&O er bygget efter Microsofts dokumenterede skema og **ikke verificeret mod kundedata** – mangler en tabel eller kolonne, er tjekket *ikke målbart* med det præcise navn i stedet for at fejle.
- **Repository:** migration 0017 (tabellerne `insight.AxBatchRuns` og `insight.AxSequenceSamples`) køres af agenten ved første start; de ryddes op efter `EventDays` og `HourlyDays` som de øvrige data.
### 1.3: Servere og databaser
- **Ændret adfærd – Dynamics AX-fund og SQLInsight Hub:** fra 1.3 sendes AX-fund (regler `COM-AX-…`) og AX-reglernes resultater **kun** til SQLInsight Hub for de databaser, hvor *AX-fund til SQLInsight Hub* er slået til under **Overvågning → Servere og databaser**. Standard er **fra** for alle databaser, også efter opgraderingen. Har I delt AX-fund med hubben i 1.2, stopper det altså ved opgraderingen, indtil I slår det til for hver AX-database. Andre fund sendes som før. Se [servere-og-databaser.md](servere-og-databaser.md#dynamics-ax-fund-og-sqlinsight-hub) og [databehandling.md](databehandling.md).
@ -164,18 +138,18 @@ Kører web i IIS, angives `"AppOffline": true` i stedet for `Service`. Så lægg
dotnet run --project tools/SQLInsight.LicenseTool -- rulepack --version 2026.10.14 --out rulepack-2026.10.14.json
# MSI til installationer lavet med MSI'en (bygges på Linux)
installer/build-msi.sh 1.5.2
installer/build-msi.sh 1.3.0
# Programpakker: én zip pr. platform med mapperne Agent/ og Web/
# Samme opsætning som MSI'en (indbygget .NET), så en opdatering ikke kræver en runtime på serveren
dotnet publish src/SQLInsight.Agent -c Release -r win-x64 --self-contained true -p:Version=1.5.2 -o pkg/win-x64/Agent
dotnet publish src/SQLInsight.Web -c Release -r win-x64 --self-contained true -p:Version=1.5.2 -o pkg/win-x64/Web
dotnet publish src/SQLInsight.Agent -c Release -r win-x64 --self-contained true -p:Version=1.3.0 -o pkg/win-x64/Agent
dotnet publish src/SQLInsight.Web -c Release -r win-x64 --self-contained true -p:Version=1.3.0 -o pkg/win-x64/Web
# Signér kanalens manifest (det, der ikke angives, overtages fra det eksisterende manifest)
dotnet run --project tools/SQLInsight.LicenseTool -- publish --key sqlinsight-k20260929.private.pem --kid k20260929 \
--feed feed --channel stable --rulepack rulepack-2026.10.14.json \
--release 1.5.2 --asset win-x64=sqlinsight-1.5.2-win-x64.zip --asset win-x64-msi=dist/SQLInsight-Pro-1.5.2-x64.msi \
--asset linux-x64=sqlinsight-1.5.2-linux-x64.zip \
--release 1.3.0 --asset win-x64=sqlinsight-1.3.0-win-x64.zip --asset win-x64-msi=dist/SQLInsight-Pro-1.3.0-x64.msi \
--asset linux-x64=sqlinsight-1.3.0-linux-x64.zip \
--notes https://git.seosoft.dk/seo/sqlinsight-pro/releases
```

View file

@ -39,7 +39,7 @@ Under **Rettigheder til agentens login** står scriptet udfyldt med det rigtige
## Databaser
Agenten finder alle databaser på serveren, når serveren tilføjes, og derefter hver time. For hver database vises tilstand, størrelse, recovery model, compatibility level, om agenten kan åbne den, og om den er en Dynamics AX-database og hvilken version (Axapta 3.0, AX 4.0, 2009, 2012, 2012 R2/R3 eller F&O, genkendt på AX' systemtabeller af samme genkendelse som AX-tjekkene; se [dynamics-ax.md](dynamics-ax.md)). Søgningen læser kun metadata og skifter aldrig database.
Agenten finder alle databaser på serveren, når serveren tilføjes, og derefter hver time. For hver database vises tilstand, størrelse, recovery model, compatibility level, om agenten kan åbne den, og om den er en Dynamics AX-database (AX 3.0 eller 4.0 og nyere, genkendt på AX' systemtabeller som AX-reglerne). Søgningen læser kun metadata og skifter aldrig database.
Valget styrer de **databasespecifikke** tjek: indstillinger, backup, CHECKDB, VLF'er, indexes, Query Store og community-regler. Instansdækkende målinger (ventetyper, CPU, blokeringer, job, fejllog, rettigheder) gælder altid hele instansen, og systemdatabaserne (master, model, msdb, tempdb) overvåges altid. Fravælges en database, løses dens åbne fund ved næste runde.

View file

@ -1,96 +0,0 @@
# Syntetiske tjek
Et syntetisk tjek gør det, en bruger eller en applikation gør – forbinder til AOS-porten, henter en side, logger på SQL Server og kører en forespørgsel, slår et navn op, logger på Dynamics AX – med jævne mellemrum, fra agentens server og som agentens konto. Så opdages det også, når **en applikationsserver ikke svarer på det syntetiske tjek, selv om tjenesten kører**: noget, en tjenesteovervågning alene aldrig ser.
Tjekkene oprettes under **Servere → Syntetiske tjek** i webbrugerfladen eller med API'et (se [nedenfor](#api)). Agenten kører dem inden for et minut efter oprettelsen og derefter efter tjekkets interval (standard hvert 5. minut, mindst hvert minut). **Kør nu** kører et tjek med det samme. Intet tjek ændrer noget på de servere, det tjekker.
## Typer
| Type | Hvad agenten gør | Succes |
|---|---|---|
| **TCP-port** | Åbner en TCP-forbindelse til server:port (fx AOS'en på 2712) | Porten svarer |
| **HTTP(S)** | `GET` på adressen, følger op til 5 omdirigeringer, eventuelt som agentens Windows-konto (Enterprise Portal, Reporting Services) | Status 200–399 eller de forventede koder, og eventuelt en forventet tekst i de første 1 MB |
| **SQL-login og forespørgsel** | Logger på SQL Server (agentens Windows-konto eller et SQL-login), skifter eventuelt database og kører forespørgslen (standard `SELECT 1`) | Login og forespørgsel lykkes; svartiden måles for login og forespørgsel hver for sig |
| **DNS-opslag** | Slår navnet op | Navnet giver adresser (og en af de forventede, hvis de er angivet) |
| **Dynamics AX-logon** | Tester AOS-porten, og logger derefter på AX gennem .NET Business Connector, hvis den er installeret på agentens server | Se nedenfor |
**HTTPS:** certifikatets udløbsdato læses ved hvert tjek – også når certifikatet afvises. Et selvsigneret certifikat kan accepteres med *Accepter ugyldigt certifikat*; udløbsdatoen tjekkes stadig.
**SQL:** forespørgslen skal være én skrivebeskyttet `SELECT` (samme regler som community-reglerne, se [regler.md](regler.md)): ingen `INSERT`, `UPDATE`, `EXEC`, `INTO` osv., ingen andre sætninger (T-SQL kræver intet semikolon mellem dem, så fx `COMMIT`, `SET`, `DECLARE`, `WRITETEXT` afvises), ingen `NEXT VALUE FOR` (sekvenser rulles ikke tilbage) og ingen låse-hints som `UPDLOCK`, `XLOCK` eller `TABLOCKX`; flere `SELECT` kun med `UNION`, `EXCEPT` eller `INTERSECT`. Kommentarer, tekster og `[navne]` læses, som SQL Server læser dem, så intet kan gemmes i dem. Højst 10.000 rækker læses. Det tjekkes, når tjekket gemmes, og igen, før det køres, og forespørgslen køres i en transaktion, der altid rulles tilbage, med `READ UNCOMMITTED` og `LOCK_TIMEOUT 5000`. Standardforespørgslen virker på SQL Server 2005 og nyere. Det præcise SQL står i [`SQL Scripts/SQLI-SYN-001.sql`](../SQL%20Scripts/SQLI-SYN-001.sql).
### Dynamics AX-logon
Tjekket gør sit bedste:
1. **AOS-porten** (standard 2712) skal svare. Gør den ikke, fejler tjekket.
2. Er **.NET Business Connector** (AX 2009 eller AX 2012) installeret på agentens server, logger agenten på AX gennem den – **i en separat Windows PowerShell-proces med timeout** (Business Connector er en .NET Framework-komponent og kan ikke indlæses i agenten) – og logger straks af igen. Der logges på som **agentens Windows-konto**, som derfor skal være oprettet som bruger i AX (i det angivne firma); der gemmes ingen adgangskode. Agenten læser intet i AX.
3. Er Business Connector **ikke** installeret (eller logon-testen slået fra), testes **kun porten**, og resultatet siger det tydeligt: *"Kun porten er testet: .NET Business Connector (AX 2009/2012) er ikke installeret på agentens server, så et AX-logon kan ikke prøves."*
Business Connector findes, hvor AX-installeren lægger den (`…\Microsoft Dynamics AX\60\BusinessConnector\Bin`, `…\50\Client\Bin`), eller hvor `SQLInsight:SyntheticChecks:BusinessConnectorPath` peger. AX 2009's Business Connector er bygget til .NET 2.0 og kræver Windows PowerShell 2.0-motoren (.NET Framework 3.5) på agentens server; mangler den, siger resultatet det. Object server angives som i AX-klientens konfiguration (fx `01@aos01:2712`); standard er server:port. Et logon optager kortvarigt en AX-session.
Kør agenten som en gMSA eller domænekonto, hvis AX-logon skal testes: computerkontoen `DOMÆNE\AGENTSERVER$` kan normalt ikke oprettes som AX-bruger.
## Tjeneste bag tjekket
Et tjek kan knyttes til en Windows-server (se [windows-servere.md](windows-servere.md)) og en tjeneste på den, fx AOS'en `AOS60$01` eller IIS `W3SVC`. Fejler tjekket, slår agenten tjenestens tilstand op, og fundet siger forskellen:
- *"AOS01 AX-logon svarer ikke, selv om tjenesten AOS60$01 kører"* – tjenesten hænger, er overbelastet, venter på databasen eller er blokeret af en firewall.
- *"AOS01 AX-logon svarer ikke: tjenesten AOS60$01 er stoppet"*.
Tjenesten læses med de samme rettigheder som Windows-serverne (Service Control Manager).
## Adgangskoder
Et SQL-logins adgangskode krypteres i webbrugerfladen i det øjeblik, den modtages, med **agentens offentlige nøgle** – præcis som i 1.3 (se [servere-og-databaser.md](servere-og-databaser.md#sql-login-adgangskoder)). Kun agenten kan læse den; den vises aldrig igen og returneres aldrig af API'et (kun `hasPassword`). Den er bundet til server, port og login: ændres de, skal den indtastes igen. Kan agenten ikke bruge den (fx efter en geninstallation), siger tjekket det, og den indtastes igen. Fjernes tjekket, slettes login og krypteret adgangskode med det samme. HTTP-, TCP-, DNS- og AX-tjek bruger ingen adgangskoder.
## Regler
| Regel | Fund | Tærskel |
|---|---|---|
| SQLI-SYN-001 | Tjekket fejler (Høj) – med tjenestens tilstand, når den kendes | `SQLInsight:SyntheticChecks:FailuresBeforeFinding` 2 fejl i træk |
| SQLI-SYN-002 | Tjekket svarer langsomt (Middel) | tjekkets *Langsomt svar over (ms)*; `SlowRunsBeforeFinding` 3 gange i træk |
| SQLI-SYN-003 | TLS-certifikatet udløber (Middel; Høj inden for `CertificateCriticalDays` 7; Kritisk når udløbet) | `CertificateWarningDays` 21 |
Ét tabt svar giver ingen alarm; to i træk gør. Fundet løses, når tjekket lykkes igen. Fund vises på tjekkets side og sammen med serverfundene, sendes som notifikationer og findes i API'et. Fundets titel nævner tjekket og tjenestens tilstand, aldrig fejlbeskeden (den står som følsom måling). Fund sendes ikke til SQLInsight Hub og indgår endnu ikke i PDF-rapporterne.
## Firewall
Fra agentens server: de porte, tjekkene bruger (fx TCP 2712 til AOS'en, 80/443 til HTTP(S), 1433 eller instansens port til SQL Server, 53 til DNS). Tjenestens tilstand: som Windows-serverne (DCOM/RPC, eller WinRM TCP 5985 med `-WinRM`).
## Opbevaring og konfiguration
Hver kørsel gemmes i 14 dage (`SQLInsight:SyntheticChecks:ResultDays`). Højst `MaxParallelChecks` (standard 4, 1–32) tjek kører samtidig, og et tjek kører aldrig to gange samtidig. Fejler et tjek uventet, gemmes det som en fejlet kørsel, og det prøves først igen efter sit interval. Timeout pr. tjek er standard 15 sekunder (AX-logon 60), højst 120.
```json
"SyntheticChecks": {
"Enabled": true,
"MaxParallelChecks": 4,
"FailuresBeforeFinding": 2,
"SlowRunsBeforeFinding": 3,
"CertificateWarningDays": 21,
"CertificateCriticalDays": 7,
"ResultDays": 14,
"BusinessConnectorPath": null
}
```
## API
| Kald | Svar |
|---|---|
| `GET /api/v1/synthetic-checks` | Tjek med seneste resultat |
| `POST /api/v1/synthetic-checks` | Opret: `name`, `kind` (`Tcp`, `Http`, `Sql`, `Dns`, `AxLogon`), `serverId`, `intervalSeconds`, `timeoutSeconds`, `settings`, `password`; 201 med `runRequestId` |
| `GET`, `PUT`, `PATCH`, `DELETE /api/v1/synthetic-checks/{id}` | Ét tjek; ret (typen kan ikke ændres); pause; fjern |
| `POST /api/v1/synthetic-checks/{id}/run` | Kør nu (202, følg `GET /api/v1/agent-requests/{id}`; venter en kørsel allerede, gives den samme; 409 for et tjek på pause). Kører tjekket allerede, venter agenten på den kørsel i stedet for at starte en til |
| `GET /api/v1/synthetic-checks/{id}/results?hours=24` | Kørsler med svartid, certifikat og tjenestens tilstand |
| `GET /api/v1/synthetic-checks/{id}/rule-states` | Reglernes udfald |
`settings` har felterne for typen: `host`, `port`, `url`, `expectedStatus`, `expectedText`, `useWindowsAuthentication`, `allowInvalidCertificate`, `server`, `database`, `authentication` (`Windows`/`SqlLogin`), `userName`, `encryption`, `trustServerCertificate`, `query`, `expectedAddresses`, `company`, `objectServer`, `configuration`, `businessConnectorLogon`, `serviceName`, `warnMilliseconds`.
```powershell
$h = @{ Authorization = "Bearer $token" }
$body = @{ name = 'AOS01 AX-logon'; kind = 'AxLogon'; serverId = 3; settings = @{ host = 'aos01'; company = 'DAT'; serviceName = 'AOS60$01' } } | ConvertTo-Json
Invoke-RestMethod https://sqlinsight.domæne.local/api/v1/synthetic-checks -Method Post -Headers $h -Body $body -ContentType 'application/json'
```
Fejlbeskeder kan nævne logins og servere: for fejlede kørsler er beskeden og følsomme målinger `[udeladt]` i API'et, medmindre `SQLInsight:Web:ApiIncludesSensitiveEvidence` er slået til. Det samme gælder `settings.userName` og en adresses forespørgselsparametre (`https://server/side?[udeladt]`): sendes et tjek tilbage med `PUT`, skal det rigtige login og den hele adresse angives igen (`[udeladt]` afvises). Adresser må højst være 2000 tegn. At ændre kræver scope `config:write` eller `SQLInsight:Web:AdminGroups`.

View file

@ -1,155 +0,0 @@
# Windows-servere
Fra version 1.5 overvåger SQLInsight også selve Windows-serverne: **CPU, hukommelse, diske, tjenester, hændelseslog og planlagte opgaver** på SQL-, applikations-/AOS- og filservere – med varsel om, at en disk løber fuld om et antal dage, før det sker. Det findes under **Servere → Windows-servere** i webbrugerfladen og i API'et (se [nedenfor](#api)).
- **SQL Server-hosts kommer med af sig selv.** Serveren bag hver instans under *Servere og databaser* (se [servere-og-databaser.md](servere-og-databaser.md)) bliver en Windows-server med rollen *SQL Server*. Den forsvinder igen, når instansen fjernes, og kan sættes på pause. Azure SQL og LocalDB springes over. Slås fra med `SQLInsight:Servers:IncludeSqlHosts`.
- **Andre servere tilføjes** i webbrugerfladen: AOS- og applikationsservere, rapport- og filservere. Angiv servernavnet (uden instans og port), rollen og eventuelt de tjenester, der altid skal køre (fx `AOS60$01`, `W3SVC`).
SQLInsight **ændrer aldrig noget** på serverne: agenten læser kun. Den starter, stopper eller genstarter ingen tjenester og rydder ikke op – fundene siger, hvad der bør gøres, og en driftsansvarlig gør det.
## Sådan læser agenten serverne
Agenten læser serverne **som sin egen konto** (den samme som til SQL Server) med CIM – det samme, PowerShells `Get-CimInstance` bruger:
1. **WinRM (WS-Man)**, TCP 5985, når kontoen har læseret i WinRM (tildelingsscriptet med `-WinRM`, Windows Server 2012 og nyere).
2. **DCOM (RPC)**, TCP 135 og dynamiske porte, hvis WinRM ikke svarer eller afviser kontoen. Det er standard med tildelingsscriptet.
3. Agentens egen server læses lokalt, uden netværk.
`SQLInsight:Servers:Protocol` kan låse det til `WSMan` eller `DCOM` (standard `Auto`). WinRM over HTTPS (TCP 5986) understøttes ikke i 1.5. Hændelsesloggen læses med hændelseslogtjenestens egen protokol (MS-EVEN6, over RPC), som filtrerer på serveren og derfor er hurtig; virker den ikke, bruges CIM (`Win32_NTLogEvent`), som er langsommere. Siden for serveren viser, hvordan den blev læst.
Kører agenten som den virtuelle konto `NT SERVICE\SQLInsight Agent` (standard), LocalSystem eller NETWORK SERVICE, kommer den til andre servere som **computerkontoen** `DOMÆNE\AGENTSERVER$`. En gMSA eller domænekonto er den samme overalt. Siden viser den konto, scriptet nedenfor skal give rettigheder til.
## Rettigheder (aldrig lokal administrator)
Agenten skal **ikke** være lokal administrator og får ingen ret til at køre kode på serverne. Den har brug for præcis dette, og intet andet:
| Rettighed | Bruges til |
|---|---|
| Lokal gruppe **Performance Monitor Users** | Ydelsestællere: CPU, hukommelse, paging, commit |
| Lokal gruppe **Event Log Readers** | Hændelseslogge, der tillader gruppen: System, Application m.fl. (aldrig Security-loggen) |
| Lokal gruppe **Distributed COM Users** | CIM over DCOM |
| WMI: **Enable Account** + **Remote Enable** på `root\cimv2` | Styresystem, oppetid, diske, tjenester |
| WMI: **Enable Account** + **Remote Enable** på `root\Microsoft\Windows\TaskScheduler` | Planlagte opgaver (Windows Server 2012 og nyere) |
| **Service Control Manager**: forbinde, liste og læse (`CC LC RP RC`) | Over netværket må ellers kun administratorer liste tjenester. Det er det samme, en lokalt logget på bruger har; ingen ret til at starte eller stoppe |
| Kun med `-WinRM`: **læseret i WinRM** (`GENERIC_READ` i RootSDDL, ingen Execute) | CIM over WinRM. Kontoen kommer *ikke* i Remote Management Users, for den gruppe giver også adgang til PowerShell Remoting |
Det hele gives af [`grant-windows-server.ps1`](../db/permissions/grant-windows-server.ps1). Webbrugerfladen viser scriptet udfyldt med agentens konto under **Rettigheder og forbindelse** på serverens side (kopiér eller hent som fil; API: `GET /api/v1/windows-servers/{id}/grant-script`). Scriptet ændrer sikkerheden på kundens server, så **afprøv det først på en server, der ikke er i produktion**, og kør det med `-WhatIf` for at se, hvad det vil gøre. En administrator kører det på serveren:
```powershell
.\grant-windows-server.ps1 -Account 'DOMÆNE\AGENTSERVER$' -WhatIf # viser planen, ændrer intet
.\grant-windows-server.ps1 -Account 'DOMÆNE\AGENTSERVER$' # DCOM (standard)
.\grant-windows-server.ps1 -Account 'DOMÆNE\AGENTSERVER$' -WinRM # også læseret i WinRM
.\grant-windows-server.ps1 -Account 'DOMÆNE\AGENTSERVER$' -EnableFirewall -AgentAddress 10.0.0.15 # egne firewallregler, kun fra agenten
.\grant-windows-server.ps1 -Account 'DOMÆNE\AGENTSERVER$' -Remove # fjerner præcis det, scriptet gav
```
Scriptet virker på Windows PowerShell 2.0 og nyere og skal køres som administrator. Grupperne findes via deres SID, så det virker på dansk og engelsk Windows. Sikkerhedsnettet:
- **Intet ændres, før alt er kontrolleret:** kontoen skal kunne slås op (brede grupper som Everyone, Authenticated Users eller Domain Users afvises), og har kontoen en *Nægt*-regel på WMI, Service Control Manager eller WinRM, stopper scriptet uden at ændre noget.
- **Kun det nødvendige:** har kontoen allerede en rettighed, røres den ikke. Hver ændring er præcis én ny regel (kontrolleret, før den skrives), og hvert skridt kontrolleres bagefter.
- **Backup:** før første ændring gemmes de oprindelige indstillinger (SDDL for Service Control Manager, WMI-navnerummene og WinRM, gruppemedlemskaber og firewallregler) i `%ProgramData%\SQLInsight\grant-windows-server\<SID>-<tid>-foer.xml`.
- **Præcis fjernelse:** hvad scriptet selv har tilføjet, står i `%ProgramData%\SQLInsight\grant-windows-server\<SID>.xml` (kun administratorer har adgang til mappen). `-Remove` fjerner kun det – et gruppemedlemskab eller en rettighed, kontoen havde før, bevares. Fejler et skridt undervejs, står det, der allerede er gjort, i filen, så scriptet kan køres igen eller med `-Remove`.
- Service Control Manager kan gendannes manuelt med `sc.exe sdset scmanager "<SDDL fra -foer.xml>"`.
Kendte begrænsninger med mindst mulige rettigheder:
- **Tjenester:** en tjeneste, hvis egen sikkerhed ikke lader kontoen læse den, ses ikke. Ser agenten slet ingen tjenester, er området "ikke målbart" med årsagen.
- **Planlagte opgaver:** agenten ser kun opgaver, kontoen må læse. Seneste resultat læses med Task Schedulers `GetInfoByName` (et skrivebeskyttet CIM-metodekald, som `Get-ScheduledTaskInfo`). Det kræver Windows' standardret *Execute Methods* på navnerummet (som Authenticated Users arver fra `root`); scriptet giver ikke mere. Over WinRM med kun læseret er metodekald ikke tilladt, så agenten læser opgaverne over DCOM (med `Protocol` = `Auto`); er protokollen låst til `WSMan`, er området "ikke målbart" med årsagen.
- **Windows Server 2008 R2:** opgave-navnerummet findes ikke, så planlagte opgaver er "ikke målbare"; brug DCOM.
### Når en rettighed mangler
Testen (**Test forbindelsen** på serverens side, eller `POST /api/v1/windows-servers/{id}/test`) udføres af agenten, som dens konto, og prøver hvert område én gang. Tabellen viser for hvert område *Kan læses* eller *Ikke målbar* med den præcise årsag, fx
> Adgang nægtet til hændelseslog på aos01.firma.local over WS-Man: Agentens konto (FIRMA\AGENT01$) mangler Event Log Readers. Kør tildelingsscriptet (grant-windows-server.ps1) som administrator på serveren, og test igen. Agenten skal ikke være lokal administrator.
Under overvågningen gælder det samme: et område, der ikke kan læses, gør kun dets regler "ikke målbare" med årsagen (under *Ikke målbart* på siden), og fund fra før forbliver åbne, indtil området kan læses igen. Problemet skrives én gang i agentens log, når det opstår eller ændrer sig – ikke hvert minut. Svarer serveren slet ikke, sendes én notifikation ("kan ikke nås") og én, når den svarer igen.
## Firewall
Fra agentens server til de overvågede servere:
| Port | Hvad | Tildelingsscriptets regel (`-EnableFirewall`) |
|---|---|---|
| TCP 135 + dynamiske RPC-porte (49152–65535) | DCOM/WMI (standard) | *SQLInsight Agent: RPC Endpoint Mapper* og *SQLInsight Agent: WMI over DCOM* |
| TCP 135 + dynamiske RPC-porte | Hændelsesloggen (MS-EVEN6) | *SQLInsight Agent: hændelseslog (RPC)* |
| TCP 5985 | WinRM (WS-Man), kun med `-WinRM` | *SQLInsight Agent: WinRM (HTTP 5985)* |
`grant-windows-server.ps1 -EnableFirewall -AgentAddress <agentens IP eller net>` opretter scriptets egne regler, kun for agentens adresse (Windows Server 2012 og nyere); `-AgentAddress` er påkrævet, så portene aldrig åbnes for alle. Windows' indbyggede regler røres ikke, og `-Remove` fjerner scriptets egne regler igen. Syntetiske tjek bruger de porte, de tjekker (se [syntetiske-tjek.md](syntetiske-tjek.md)).
## Hvad måles, og hvor ofte
| Område | Interval | Hvad |
|---|---|---|
| CPU, hukommelse, oppetid | 1 minut | Gennemsnitlig CPU pr. minut (rå tællere, ikke et øjebliksbillede), ledig hukommelse, paging, commit, starttidspunkt |
| Diske | 15 minutter | Ledig plads pr. drev og monteringspunkt; prognose for, hvornår drevet er fuldt |
| Tjenester | 5 minutter | Alle tjenester med status og starttype |
| Hændelseslog | 5 minutter | Fejl og kritiske hændelser i System og Application siden sidst (første gang det seneste døgn) |
| Planlagte opgaver | 15 minutter | Opgaver uden for `\Microsoft\` med seneste kørsel, resultat og næste kørsel |
Intervallerne ændres under `SQLInsight:Servers:Intervals` (`performance`, `disks`, `services`, `events`, `tasks`). Højst `MaxParallelServers` (standard 4, 1–32) servere læses ad gangen. Hver CIM-operation har en timeout (`OperationTimeout`, 5 sekunder–5 minutter), og en hel indsamling for én server højst `CycleTimeout` (standard 5 minutter, 1–30); når den er brugt, er resten "ikke målbart" til næste gang, og planlagte opgaver har en tredjedel af tiden. En server, der hænger eller ikke svarer, kan derfor aldrig holde agenten op. Svarer den ikke, prøves den igen efter 1, 2, 5, 10 og derefter hvert 15. minut, og den protokol, der virkede, huskes en time.
### "Disken er fuld om ca. N dage"
Ledig plads pr. drev gemmes hvert 15. minut i SQLInsights repository. Prognosen lægger en ret linje (mindste kvadraters metode) gennem timegennemsnittene for de seneste 14 dage (`Disks:ForecastHistoryDays`) – kun siden drevet sidst skiftede størrelse, så et udvidet drev starter forfra. Der gives kun en prognose, når der er mindst to dages historik og 12 målinger, linjen passer rimeligt (R² mindst 0,5), drevet faktisk fyldes (mindst 0,01 GB om dagen), og den rammer inden for et år. Et backupdrev, der fyldes om natten og ryddes om morgenen, giver derfor ingen alarm. Fundet kommer, når drevet forventes fuldt inden for 30 dage (`Disks:ForecastDays`): Middel, inden for 7 dage Høj, inden for 2 dage Kritisk.
### Hændelsesloggen
Som SQL Servers fejllog (se [fejllog.md](fejllog.md)) grupperes hændelserne pr. log, kilde og hændelses-id med antal, første og seneste tidspunkt og den nyeste tekst som eksempel. Kendt støj sorteres fra (DCOM 10016/10010, Schannel-alarmer, TPM-WMI, WMI 10 m.fl.), og det samme gør SQL Servers egne kilder (`MSSQLSERVER`, `MSSQL$…`, `SQLSERVERAGENT` …), som SQL Server-overvågningen allerede dækker. Egen støj tilføjes under `EventLog:Ignore` som `"Kilde:id"`, `"Kilde"` eller `"Kilde*"`. Disk- og filsystemfejl, opbrugte ressourcer og kritiske hændelser giver fund med det samme; tjenester der går ned, programnedbrud og Dynamics AX' AOS-fejl også; almindelige fejl først, når de er set 5 gange på et døgn (`EventLog:RepeatThreshold`). Et fund forbliver åbent et døgn efter hændelsen sidst blev set. Eksempelteksten kan nævne brugere, computere og stier: den vises i webbrugerfladen, men er `[udeladt]` i API'et.
## Regler
| Regel | Fund | Tærskel (`SQLInsight:Servers:…`) |
|---|---|---|
| SQLI-WIN-001 | CPU højt i længere tid (Høj; Kritisk fra 98 %) | `Cpu:HighPercent` 90 % i `Cpu:SustainedMinutes` 15 min. |
| SQLI-WIN-002 | Næsten ingen ledig hukommelse (Middel; Høj under 2 % og 256 MB) | under `Memory:AvailablePercent` 5 % **og** `Memory:AvailableMegabytes` 1024 MB (5 min.) |
| SQLI-WIN-003 | Paging med lidt ledig hukommelse | `Memory:PagesPerSecond` 1000 (15 min.) og under 10 % ledig (fast i koden) |
| SQLI-WIN-004 | Commit tæt på grænsen (Middel; Høj fra 97 %) | `Memory:CommitPercent` 90 % |
| SQLI-WIN-005 | Lidt ledig plads på et drev | under `Disks:FreePercent` 10 % (Høj under `CriticalFreePercent` 5 %, Kritisk under 1 GB eller 1 %), medmindre mindst `EnoughFreeGigabytes` 50 GB er ledig |
| SQLI-WIN-006 | Drevet er fuldt om ca. N dage ("ikke målbar", indtil der er historik nok) | `Disks:ForecastDays` 30 |
| SQLI-WIN-007 | Automatisk tjeneste kører ikke | `Services:Ignore` (navn eller visningsnavn, `*` til sidst), `Services:StartupGraceMinutes` 15 ("ikke målbar" lige efter en genstart, så åbne fund bliver stående), `Services:MaxFindings` 15 (resten samles i ét fund); tjenester der stopper af sig selv (sppsvc, opdateringstjenester …) giver ingen fund |
| SQLI-WIN-008 | Overvåget tjeneste kører ikke (eller findes ikke) | listen pr. server |
| SQLI-WIN-009 | Fejl i hændelsesloggen | `EventLog:RepeatThreshold` 5 inden for de seneste 24 timer (talt pr. time), `EventLog:MaxFindings` 15 (resten samles i ét fund) |
| SQLI-WIN-010 | Serveren er genstartet (uventet: Høj; planlagt: Info i et døgn; Lav, hvis hændelsesloggen ikke kan læses; "ikke målbar", til loggen er læst efter genstarten) | Kernel-Power 41, EventLog 6008, BugCheck 1001 |
| SQLI-WIN-011 | Ikke genstartet i lang tid | `UptimeDays` (standard 0 = slået fra) |
| SQLI-WIN-012 | Planlagt opgave fejlede | `Tasks:IncludeMicrosoftTasks`, `Tasks:Ignore`, `Tasks:MaxWatched` 50, `Tasks:IgnoreUserAndUpdaterTasks` (standard til: opgaver pr. bruger med SID i navnet, OneDrive-, Edge- og Google-opdatering) |
| SQLI-WIN-013 | Planlagt opgave kørte ikke som planlagt | `Tasks:LateMinutes` 60 |
Hver regel har sit eget script i [`SQL Scripts/`](../SQL%20Scripts/) (fx `SQLI-WIN-006.sql`) med de præcise CIM/WQL-forespørgsler og en skrivebeskyttet PowerShell-linje, der efterprøver dem.
Fund på servere vises på serverens side, under **Aktive fund på servere og syntetiske tjek** på siden Windows-servere, sendes som notifikationer (med link til fundet) og findes i API'et. De sendes ikke til SQLInsight Hub og indgår endnu ikke i PDF-rapporterne.
## Opbevaring
| Data | Standard | Indstilling |
|---|---|---|
| CPU og hukommelse pr. minut | 14 dage | `SQLInsight:Servers:SampleDays` |
| Ledig plads pr. drev | 35 dage (mindst prognosens historik) | `SQLInsight:Servers:DiskHistoryDays` |
| Grupperede hændelser | 90 dage | `SQLInsight:Servers:EventDays` |
| Løste fund | 365 dage | `SQLInsight:Retention:ResolvedFindingDays` |
En server med 5 drev fylder omkring 1.440 rækker om dagen for CPU/hukommelse og 480 for diskene. Agenten sletter i små portioner hver time.
## Konfiguration
Alle tærskler, intervaller og opbevaringstider står i agentens `appsettings.json` under `SQLInsight:Servers`. Ændringer kræver en genstart af agenten. Servere, roller og tjenestelister ændres i webbrugerfladen og bruges inden for et minut.
## API
| Kald | Svar |
|---|---|
| `GET /api/v1/windows-servers` | Servere med status, styresystem, protokol, områdernes målbarhed, seneste CPU/hukommelse, fyldigste drev, nærmeste "fuld"-prognose og antal fund |
| `POST /api/v1/windows-servers` | Tilføj (`host`, `name`, `role`, `watchedServices`, `ignoredServices`); 201 med `serverTestRequestId` |
| `GET`, `PATCH`, `DELETE /api/v1/windows-servers/{id}` | Én server; pause, rolle og tjenestelister; fjern (409 for SQL Server-hosts) |
| `POST`, `GET /api/v1/windows-servers/{id}/test` | Test forbindelse og rettigheder (202, følg `GET /api/v1/agent-requests/{id}`; venter en test allerede, gives den samme); seneste test med manglende områder |
| `GET /api/v1/windows-servers/{id}/grant-script` | Scriptet udfyldt med agentens konto (kontoen er `[udeladt]`, og scriptet er skabelonen, medmindre følsomme data er slået til) |
| `GET /api/v1/windows-servers/{id}/performance?hours=24` | CPU og hukommelse pr. minut |
| `GET /api/v1/windows-servers/{id}/disks` | Drev med ledig plads og prognose |
| `GET /api/v1/windows-servers/{id}/services` | Tjenester og serverens lister |
| `GET /api/v1/windows-servers/{id}/events?days=1` | `capturedAt`, `method`, `ignored`, `coverage` og de grupperede hændelser (eksemplet er `[udeladt]`) |
| `GET /api/v1/windows-servers/{id}/tasks` | Overvågede planlagte opgaver |
| `GET /api/v1/windows-servers/{id}/rule-states` | Reglernes udfald med årsag |
| `GET /api/v1/server-findings?status=&scope=server&id=&limit=&continuation=` | Fund på servere og syntetiske tjek, højst `limit` (standard 100, højst 500) ad gangen, næste side via `X-Continuation`. Aktive fund på servere og tjek på pause eller fjernede er ikke med, medmindre `scope` og `id` angives |
| `GET /api/v1/server-findings/{id}` | Ét fund med tidligere forekomster (`history`) |
At ændre kræver scope `config:write` (API-token) eller medlemskab af `SQLInsight:Web:AdminGroups` (Windows-brugere; er den tom, den lokale gruppe Administratorer på webserveren), som for *Servere og databaser*. Hvem der har oprettet og ændret, og agentens konto i fejltekster, er `[udeladt]`, medmindre `SQLInsight:Web:ApiIncludesSensitiveEvidence` er slået til. Fjernes en server, beholdes dens syntetiske tjek, men uden server og tjeneste.

View file

@ -1,10 +1,10 @@
// Microsoft Dynamics AX (Axapta), with emphasis on AX 3.0.
//
// The agent has no data access (grant-least-privilege.sql), so these rules only read metadata and DMVs. They are marked
// "dynamicsAx": the agent runs them only in databases its central detection (Core/DynamicsAx/AxDetection.cs) recognises as
// Dynamics AX (SQLDICTIONARY and DATAAREA in one schema: bmssa for AX 3.0, dbo later) and that are chosen for monitoring
// in Servere og databaser, and their findings only reach SQLInsight Hub for databases where AX findings may be sent. AX 3.0 is
// told apart from AX 4.0 and later by RECID being 32-bit (int) instead of 64-bit (bigint).
// The agent has no data access (grant-least-privilege.sql), so these rules only read metadata and DMVs. They run in
// every user database, so an AX database is recognised without naming its tables directly (that would fail to compile
// elsewhere): the AX system tables SQLDICTIONARY, DATAAREA and SYSCONFIG in sys.tables, in ANY schema: AX 3.0 owns its
// tables as the database user bmssa (bmssa.SQLDICTIONARY), later versions use dbo. AX 3.0 is told apart from AX 4.0
// and later by RECID being 32-bit (int) instead of 64-bit (bigint).
//
// Everything is SQL Server 2005 syntax (no CONCAT, IIF or STRING_AGG): AX 3.0 typically runs on 2000 or 2005.
[
@ -13,8 +13,7 @@
"title": "Dynamics AX 3.0 er uden support fra Microsoft",
"severity": "Medium",
"scope": "Database",
"dynamicsAx": true,
"query": "SELECT CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)) AS [sql_version] WHERE COLUMNPROPERTY((SELECT TOP 1 object_id FROM sys.tables WHERE name = N'SQLDICTIONARY'), N'RECID', 'Precision') = 10",
"query": "SELECT CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)) AS [sql_version] WHERE EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SQLDICTIONARY') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'DATAAREA') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SYSCONFIG') AND COLUMNPROPERTY((SELECT TOP 1 object_id FROM sys.tables WHERE name = N'SQLDICTIONARY'), N'RECID', 'Precision') = 10",
"subject": "database: {database}",
"findingTitle": "{database} er en Dynamics AX 3.0-database, og AX 3.0 er uden support",
"explanation": "Microsoft Dynamics AX 3.0 er for længst ude af Microsofts support, også den udvidede. Der kommer ingen sikkerhedsopdateringer eller rettelser til AX-kernen, klienten eller AOS, og nyere Windows- og SQL Server-versioner er aldrig testet med AX 3.0. AX 3.0 bruger desuden 32-bit RecId (int), som kan løbe tør på store installationer.",
@ -27,8 +26,7 @@
"title": "Dynamics AX 3.0 kører på en SQL Server-version, der ikke var understøttet til AX 3.0",
"severity": "Medium",
"scope": "Database",
"dynamicsAx": true,
"query": "SELECT CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)) AS [sql_version], d.compatibility_level AS [compatibility_level] FROM sys.databases d WHERE d.database_id = DB_ID() AND CAST(PARSENAME(CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)), 4) AS int) > 9 AND COLUMNPROPERTY((SELECT TOP 1 object_id FROM sys.tables WHERE name = N'SQLDICTIONARY'), N'RECID', 'Precision') = 10",
"query": "SELECT CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)) AS [sql_version], d.compatibility_level AS [compatibility_level] FROM sys.databases d WHERE d.database_id = DB_ID() AND CAST(PARSENAME(CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)), 4) AS int) > 9 AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SQLDICTIONARY') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'DATAAREA') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SYSCONFIG') AND COLUMNPROPERTY((SELECT TOP 1 object_id FROM sys.tables WHERE name = N'SQLDICTIONARY'), N'RECID', 'Precision') = 10",
"subject": "database: {database}",
"findingTitle": "AX 3.0-databasen {database} ligger på SQL Server {sql_version}",
"explanation": "AX 3.0 blev kun frigivet til SQL Server 2000 og 2005. På nyere versioner kan den virke, men optimizeren, låsning og udfasede funktioner opfører sig anderledes end det, AX-kernen blev bygget til, og hverken Microsoft eller en AX-partner kan give support. Fejl viser sig ofte først under belastning, fx ved månedsafslutning. Databasens compatibility level er {compatibility_level}.",
@ -41,8 +39,7 @@
"title": "Dynamics AX-database på en instans, hvor MAXDOP ikke er 1 (SQL Server 2005-2014)",
"severity": "Medium",
"scope": "Database",
"dynamicsAx": true,
"query": "SELECT CAST(c.value_in_use AS int) AS [maxdop] FROM sys.configurations c WHERE c.name = 'max degree of parallelism' AND CAST(c.value_in_use AS int) <> 1 AND CAST(PARSENAME(CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)), 4) AS int) < 13",
"query": "SELECT CAST(c.value_in_use AS int) AS [maxdop] FROM sys.configurations c WHERE c.name = 'max degree of parallelism' AND CAST(c.value_in_use AS int) <> 1 AND CAST(PARSENAME(CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)), 4) AS int) < 13 AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SQLDICTIONARY') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'DATAAREA') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SYSCONFIG')",
"subject": "database: {database}",
"findingTitle": "MAXDOP er {maxdop} på instansen med Dynamics AX-databasen {database}",
"explanation": "Dynamics AX sender mange små, hyppige forespørgsler og cursorer fra AOS. Når de får parallelle planer, bruger de flere tråde end nødvendigt, venter på hinanden (CXPACKET) og holder låse længere, så brugerne oplever langsomme formularer og blokering. Microsofts vejledning for Dynamics AX er derfor max degree of parallelism = 1 på instansen.",
@ -58,9 +55,8 @@
"title": "Dynamics AX-database, hvor MAXDOP ikke er 1 (SQL Server 2016 og nyere)",
"severity": "Medium",
"scope": "Database",
"dynamicsAx": true,
"minimumVersion": "13.0",
"query": "SELECT CAST(c.value_in_use AS int) AS [server_maxdop], CAST(s.value AS int) AS [database_maxdop] FROM sys.configurations c CROSS JOIN sys.database_scoped_configurations s WHERE c.name = 'max degree of parallelism' AND s.name = 'MAXDOP' AND (CASE WHEN CAST(s.value AS int) = 0 THEN CAST(c.value_in_use AS int) ELSE CAST(s.value AS int) END) <> 1",
"query": "SELECT CAST(c.value_in_use AS int) AS [server_maxdop], CAST(s.value AS int) AS [database_maxdop] FROM sys.configurations c CROSS JOIN sys.database_scoped_configurations s WHERE c.name = 'max degree of parallelism' AND s.name = 'MAXDOP' AND (CASE WHEN CAST(s.value AS int) = 0 THEN CAST(c.value_in_use AS int) ELSE CAST(s.value AS int) END) <> 1 AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SQLDICTIONARY') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'DATAAREA') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SYSCONFIG')",
"subject": "database: {database}",
"findingTitle": "Dynamics AX-databasen {database} kører med MAXDOP større end 1",
"explanation": "Dynamics AX sender mange små, hyppige forespørgsler og cursorer fra AOS. Når de får parallelle planer, bruger de flere tråde end nødvendigt, venter på hinanden (CXPACKET) og holder låse længere. Microsofts vejledning for Dynamics AX er MAXDOP 1. Instansen har {server_maxdop}, og databasen har {database_maxdop} (0 = instansens værdi gælder).",
@ -76,8 +72,7 @@
"title": "Dynamics AX-tabeller uden unikt RecId-indeks",
"severity": "Medium",
"scope": "Database",
"dynamicsAx": true,
"query": "SELECT COUNT(*) AS [tables], LEFT(STUFF((SELECT TOP 10 N', ' + t2.name FROM sys.tables t2 WHERE COLUMNPROPERTY(t2.object_id, N'RECID', 'ColumnId') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys.indexes i JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.is_included_column = 0 JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id WHERE i.object_id = t2.object_id AND i.is_unique = 1 AND c.name = N'RECID') ORDER BY t2.name FOR XML PATH('')), 1, 2, ''), 400) AS [examples] FROM sys.tables t WHERE COLUMNPROPERTY(t.object_id, N'RECID', 'ColumnId') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys.indexes i JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.is_included_column = 0 JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id WHERE i.object_id = t.object_id AND i.is_unique = 1 AND c.name = N'RECID') HAVING COUNT(*) > 0",
"query": "SELECT COUNT(*) AS [tables], LEFT(STUFF((SELECT TOP 10 N', ' + t2.name FROM sys.tables t2 WHERE COLUMNPROPERTY(t2.object_id, N'RECID', 'ColumnId') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys.indexes i JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.is_included_column = 0 JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id WHERE i.object_id = t2.object_id AND i.is_unique = 1 AND c.name = N'RECID') ORDER BY t2.name FOR XML PATH('')), 1, 2, ''), 400) AS [examples] FROM sys.tables t WHERE COLUMNPROPERTY(t.object_id, N'RECID', 'ColumnId') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys.indexes i JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.is_included_column = 0 JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id WHERE i.object_id = t.object_id AND i.is_unique = 1 AND c.name = N'RECID') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SQLDICTIONARY') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'DATAAREA') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SYSCONFIG') HAVING COUNT(*) > 0",
"subject": "database: {database}",
"findingTitle": "{tables} AX-tabeller i {database} har intet unikt indeks på RECID",
"explanation": "AX identificerer hver post med RecId og opretter normalt et unikt indeks på den (I_<tabel-id>RECID) ved synkronisering. Mangler det, er en synkronisering typisk fejlet eller afbrudt, eller indekset er fjernet manuelt. Uden det bliver opslag på RecId (som AX laver hele tiden, fx ved opdatering og sletning af én post) til scanninger, og dubletter opdages ikke. Eksempler: {examples}.",
@ -90,8 +85,7 @@
"title": "Store Dynamics AX-tabeller uden clustered index (heaps)",
"severity": "Medium",
"scope": "Database",
"dynamicsAx": true,
"query": "SELECT COUNT(*) AS [tables], SUM(h.row_count) AS [rows], LEFT(STUFF((SELECT TOP 10 N', ' + OBJECT_NAME(h2.object_id) + N' (' + CAST(h2.row_count AS nvarchar(20)) + N')' FROM (SELECT p.object_id, SUM(p.rows) AS row_count FROM sys.partitions p JOIN sys.tables t ON t.object_id = p.object_id WHERE p.index_id = 0 GROUP BY p.object_id) h2 WHERE h2.row_count >= 100000 ORDER BY h2.row_count DESC FOR XML PATH('')), 1, 2, ''), 400) AS [largest] FROM (SELECT p.object_id, SUM(p.rows) AS row_count FROM sys.partitions p JOIN sys.tables t ON t.object_id = p.object_id WHERE p.index_id = 0 GROUP BY p.object_id) h WHERE h.row_count >= 100000 HAVING COUNT(*) > 0",
"query": "SELECT COUNT(*) AS [tables], SUM(h.row_count) AS [rows], LEFT(STUFF((SELECT TOP 10 N', ' + OBJECT_NAME(h2.object_id) + N' (' + CAST(h2.row_count AS nvarchar(20)) + N')' FROM (SELECT p.object_id, SUM(p.rows) AS row_count FROM sys.partitions p JOIN sys.tables t ON t.object_id = p.object_id WHERE p.index_id = 0 GROUP BY p.object_id) h2 WHERE h2.row_count >= 100000 ORDER BY h2.row_count DESC FOR XML PATH('')), 1, 2, ''), 400) AS [largest] FROM (SELECT p.object_id, SUM(p.rows) AS row_count FROM sys.partitions p JOIN sys.tables t ON t.object_id = p.object_id WHERE p.index_id = 0 GROUP BY p.object_id) h WHERE h.row_count >= 100000 AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SQLDICTIONARY') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'DATAAREA') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SYSCONFIG') HAVING COUNT(*) > 0",
"subject": "database: {database}",
"findingTitle": "{tables} store AX-tabeller i {database} har intet clustered index",
"explanation": "En tabel uden clustered index (en heap) får spredte data og 'forwarded records', når poster opdateres, og den kan ikke defragmenteres med en indeks-rebuild. I AX styres det af tabellens ClusterIndex-egenskab i AOT, som i AX 3.0 ofte ikke er sat på standard- og kundetabeller. På store tabeller giver det langsomme opslag og mere I/O. I alt {rows} rækker. Største: {largest}.",
@ -104,8 +98,7 @@
"title": "Automatiske statistikker er slået fra i en Dynamics AX-database",
"severity": "High",
"scope": "Database",
"dynamicsAx": true,
"query": "SELECT CASE WHEN d.is_auto_create_stats_on = 1 THEN 'til' ELSE 'fra' END AS [auto_create_statistics], CASE WHEN d.is_auto_update_stats_on = 1 THEN 'til' ELSE 'fra' END AS [auto_update_statistics] FROM sys.databases d WHERE d.database_id = DB_ID() AND (d.is_auto_create_stats_on = 0 OR d.is_auto_update_stats_on = 0)",
"query": "SELECT CASE WHEN d.is_auto_create_stats_on = 1 THEN 'til' ELSE 'fra' END AS [auto_create_statistics], CASE WHEN d.is_auto_update_stats_on = 1 THEN 'til' ELSE 'fra' END AS [auto_update_statistics] FROM sys.databases d WHERE d.database_id = DB_ID() AND (d.is_auto_create_stats_on = 0 OR d.is_auto_update_stats_on = 0) AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SQLDICTIONARY') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'DATAAREA') AND EXISTS (SELECT 1 FROM sys.tables WHERE name = N'SYSCONFIG')",
"subject": "database: {database}",
"findingTitle": "Automatiske statistikker er slået fra i AX-databasen {database}",
"explanation": "AX bygger sine forespørgsler dynamisk ud fra brugernes filtre, så optimizeren er helt afhængig af, at statistikkerne er der og er opdaterede. Uden automatiske statistikker vælger SQL Server ofte forkerte planer, især efter store kørsler som lageropgørelse og nummerserier, og formularer og batchjob kan pludselig blive meget langsomme. Auto create statistics: {auto_create_statistics}. Auto update statistics: {auto_update_statistics}.",

View file

@ -30,8 +30,6 @@ public sealed class AgentOptions
public TempdbOptions Tempdb { get; set; } = new();
public DynamicsAxOptions DynamicsAx { get; set; } = new();
public IList<MonitoredInstance> Instances { get; } = [];
}
@ -124,90 +122,3 @@ public sealed class TempdbOptions
/// <summary>A session using more tempdb than this (MB) for 15 minutes is a finding (SQLI-TEMP-003).</summary>
public int SessionMegabytes { get; set; } = 1024;
}
/// <summary>
/// The Dynamics AX checks (SQLInsight:DynamicsAx). They only run in databases recognised as AX and chosen for monitoring;
/// the data checks need the optional read access (db/permissions/grant-dynamics-ax-read.sql).
/// </summary>
public sealed class DynamicsAxOptions
{
/// <summary>A waiting batch job this many minutes past its planned start is late (SQLI-AX-003).</summary>
public int BatchLateMinutes { get; set; } = 30;
/// <summary>Late by this many hours: in practice never started (High).</summary>
public int BatchNeverStartedHours { get; set; } = 24;
/// <summary>A waiting job planned more than this many days ago is abandoned and not reported as late.</summary>
public int BatchAbandonedDays { get; set; } = 30;
/// <summary>A batch job executing this many hours is stuck (SQLI-AX-004).</summary>
public int BatchStuckHours { get; set; } = 6;
/// <summary>Running this many times longer than its own 90th percentile is unusually long (SQLI-AX-002).</summary>
public double BatchLongRunningFactor { get; set; } = 2.0;
/// <summary>…but never for runs shorter than this (minutes).</summary>
public int BatchLongRunningMinimumMinutes { get; set; } = 10;
/// <summary>Earlier runs needed before a job is compared with its own history.</summary>
public int BatchHistoryMinimumRuns { get; set; } = 5;
/// <summary>A run that failed within this many hours is reported (SQLI-AX-001).</summary>
public int BatchFailedLookbackHours { get; set; } = 24;
/// <summary>A number sequence used more than this (percent) is a finding (SQLI-AX-005).</summary>
public double NumberSequenceWarnPercent { get; set; } = 80;
/// <summary>…and High from this.</summary>
public double NumberSequenceCriticalPercent { get; set; } = 95;
/// <summary>Forecast to run out within this many days is a finding (number sequences and 64-bit RecId).</summary>
public int ForecastWarnDays { get; set; } = 90;
/// <summary>…and High within this many days.</summary>
public int ForecastCriticalDays { get; set; } = 30;
/// <summary>AX 3.0's 32-bit RecId: a finding from this share of the int range (SQLI-AX-009).</summary>
public double RecId32WarnPercent { get; set; } = 50;
/// <summary>…and Critical from this.</summary>
public double RecId32CriticalPercent { get; set; } = 75;
/// <summary>AX 3.0: forecast to run out within this many days is a finding, Critical within half.</summary>
public int RecId32ForecastDays { get; set; } = 730;
/// <summary>64-bit RecId (or a lowered MAXVAL): a finding from this share.</summary>
public double RecId64WarnPercent { get; set; } = 80;
/// <summary>An AX session blocking others this many seconds is reported with its AX user (SQLI-AX-010).</summary>
public int BlockingSeconds { get; set; } = 30;
/// <summary>How many days back the forecasts look.</summary>
public int ForecastBasisDays { get; set; } = 30;
/// <summary>The AOS's time zone (Windows or IANA id) for Axapta 3.0/4.0's local times; empty: the agent's own zone when it matches the SQL Server.</summary>
public string? LocalTimeZone { get; set; }
public Core.DynamicsAx.AxThresholds ToThresholds() => new()
{
BatchLateMinutes = Math.Max(1, BatchLateMinutes),
BatchNeverStartedHours = Math.Max(1, BatchNeverStartedHours),
BatchAbandonedDays = Math.Max(2, BatchAbandonedDays),
BatchStuckHours = Math.Max(1, BatchStuckHours),
BatchLongRunningFactor = Math.Max(1.1, BatchLongRunningFactor),
BatchLongRunningMinimumMinutes = Math.Max(1, BatchLongRunningMinimumMinutes),
BatchHistoryMinimumRuns = Math.Clamp(BatchHistoryMinimumRuns, 1, Core.DynamicsAx.AxBatchHistory.RunsKept),
BatchFailedLookbackHours = Math.Max(1, BatchFailedLookbackHours),
NumberSequenceWarnPercent = Math.Clamp(NumberSequenceWarnPercent, 1, 100),
NumberSequenceCriticalPercent = Math.Clamp(NumberSequenceCriticalPercent, 1, 100),
ForecastWarnDays = Math.Max(1, ForecastWarnDays),
ForecastCriticalDays = Math.Max(1, ForecastCriticalDays),
RecId32WarnPercent = Math.Clamp(RecId32WarnPercent, 1, 100),
RecId32CriticalPercent = Math.Clamp(RecId32CriticalPercent, 1, 100),
RecId32ForecastDays = Math.Max(1, RecId32ForecastDays),
RecId64WarnPercent = Math.Clamp(RecId64WarnPercent, 1, 100),
BlockingSeconds = Math.Max(1, BlockingSeconds),
ForecastBasisDays = Math.Clamp(ForecastBasisDays, 2, 365),
LocalTimeZone = string.IsNullOrWhiteSpace(LocalTimeZone) ? null : LocalTimeZone.Trim(),
};
}

View file

@ -87,9 +87,6 @@ public sealed partial class AgentCredentialKey : IDisposable
public string Open(byte[] envelope, string server, string? userName) =>
CredentialEnvelope.Open(envelope, KeyId, _rsa, CredentialEnvelope.Context(server, userName));
/// <summary>Opens a secret sealed for this agent and the given context (e.g. a synthetic check's server and login).</summary>
public string OpenContext(byte[] envelope, string context) => CredentialEnvelope.Open(envelope, KeyId, _rsa, context);
/// <summary>What the agent publishes in insight.AgentKeys: the key, and the account and computer it runs as.</summary>
public AgentKeyRow Describe(DateTimeOffset now) => new()
{

View file

@ -31,8 +31,7 @@ public sealed partial class AgentRequestService(
ISqlConnectionFactory connectionFactory,
RepositoryReadiness readiness,
TimeProvider timeProvider,
ILogger<AgentRequestService> logger,
Servers.ServerRequestHandler? serverRequests = null) : BackgroundService
ILogger<AgentRequestService> logger) : BackgroundService
{
/// <summary>How often the queue is looked at. Short, so the web UI gets an answer within seconds.</summary>
public static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(2);
@ -61,7 +60,7 @@ public sealed partial class AgentRequestService(
{
nextHousekeeping = now.AddMinutes(10);
await repository.PublishAgentKeyAsync(key.Describe(now), stoppingToken).ConfigureAwait(false);
await repository.CleanUpAgentRequestsAsync(now, StaleAfter, TimeSpan.FromMinutes(30), TimeSpan.FromDays(30), stoppingToken)
await repository.CleanUpAgentRequestsAsync(now, RequestTimeout + TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(30), TimeSpan.FromDays(30), stoppingToken)
.ConfigureAwait(false);
}
@ -86,9 +85,8 @@ public sealed partial class AgentRequestService(
public async Task HandleAsync(AgentRequestRow request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var limit = TimeoutOf(request.Kind);
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeout.CancelAfter(limit);
timeout.CancelAfter(RequestTimeout);
string? result = null;
string? error = null;
try
@ -99,13 +97,12 @@ public sealed partial class AgentRequestService(
AgentRequestKind.DatabaseDiscovery => AgentRequestJson.Serialize(await DiscoverAsync(Require(request), timeout.Token).ConfigureAwait(false)),
AgentRequestKind.InstanceDiscovery => AgentRequestJson.Serialize(await InstanceFinder.FindAsync(
AgentRequestJson.Deserialize<InstanceDiscoveryParameters>(request.Parameters)?.Host, TimeSpan.FromSeconds(3), timeout.Token).ConfigureAwait(false)),
_ when serverRequests is not null && Servers.ServerRequestHandler.Handles(request.Kind) => await serverRequests.HandleAsync(request, timeout.Token).ConfigureAwait(false),
_ => throw new InvalidOperationException($"Ukendt anmodning: {request.Kind}."),
};
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
error = $"Agenten fik ikke svar inden for {limit.TotalMinutes:0} minutter.";
error = $"Agenten fik ikke svar inden for {RequestTimeout.TotalMinutes:0} minutter.";
}
// Anything else (a lock timeout, a repository error while storing a discovery …) is the request's result too, so it
// is never left as Running for the web UI to wait on.
@ -118,15 +115,6 @@ public sealed partial class AgentRequestService(
await repository.CompleteAgentRequestAsync(request.RequestId, error is null, result, error, timeProvider.GetUtcNow(), CancellationToken.None).ConfigureAwait(false);
}
/// <summary>How long a request may take: a synthetic check run now may take its own timeout plus the lookup of its service.</summary>
public TimeSpan TimeoutOf(string kind) =>
serverRequests is not null && Servers.ServerRequestHandler.Handles(kind) ? Longest(RequestTimeout, Servers.ServerRequestHandler.RequestTimeout) : RequestTimeout;
/// <summary>A request still Running after this was lost (the agent stopped): the longest request plus a minute.</summary>
private static TimeSpan StaleAfter => Longest(RequestTimeout, Servers.ServerRequestHandler.RequestTimeout) + TimeSpan.FromMinutes(1);
private static TimeSpan Longest(TimeSpan a, TimeSpan b) => a > b ? a : b;
private static int Require(AgentRequestRow request) =>
request.ConfiguredInstanceId ?? throw new InvalidOperationException("Anmodningen mangler en instans.");

View file

@ -10,9 +10,6 @@ namespace SQLInsight.Agent.Hub;
/// </summary>
public static class SubmissionBuilder
{
/// <summary>What the hub gets instead of an AX rule's reason (which can name databases, tables and logins).</summary>
public const string AxReasonKeptLocally = "Detaljer om Dynamics AX bliver i kundens netværk.";
public static readonly string AgentVersion =
typeof(SubmissionBuilder).Assembly.GetName().Version?.ToString() ?? "0.0.0";
@ -48,14 +45,7 @@ public static class SubmissionBuilder
Edition = info.Edition,
ServerName = options.SendServerName ? info.ServerName : null,
},
// An AX rule's outcome covers every AX database on the instance, and its reason can name databases, tables, logins
// and the GRANT text: only the outcome goes to the hub, with a fixed reason.
Rules = results.Where(r => axRulesAllowed || !Core.Configuration.DynamicsAxFindings.IsDynamicsAxRule(r.RuleId)).Select(r => new RuleOutcomeDto
{
RuleId = r.RuleId,
Outcome = r.Outcome.ToString(),
Reason = Core.Configuration.DynamicsAxFindings.IsDynamicsAxRule(r.RuleId) && r.Reason is not null ? AxReasonKeptLocally : r.Reason,
}).ToList(),
Rules = results.Where(r => axRulesAllowed || !Core.Configuration.DynamicsAxFindings.IsDynamicsAxRule(r.RuleId)).Select(r => new RuleOutcomeDto { RuleId = r.RuleId, Outcome = r.Outcome.ToString(), Reason = r.Reason }).ToList(),
Findings = activeFindings.Where(f => Allowed(f.RuleId, f.Subject)).Select(f => new FindingDto
{
RuleId = f.RuleId,

View file

@ -267,32 +267,6 @@ public sealed partial class InstanceMonitor(
return await repository.GetSnapshotAsync<Core.Data.IndexSnapshot>(instanceId, collector.Name, cancellationToken).ConfigureAwait(false);
}
// Dynamics AX: each batch job's own run history, the first-seen times of running jobs, the history watermark and the
// sequences' daily samples come back from the repository, so comparisons and forecasts continue after a restart.
if (collector is Collectors.DynamicsAx.AxBatchCollector)
{
var stored = await repository.GetSnapshotAsync<Core.DynamicsAx.AxBatchSnapshot>(instanceId, collector.Name, cancellationToken).ConfigureAwait(false)
?? new Core.DynamicsAx.AxBatchSnapshot { CapturedAt = now, Thresholds = new(), Databases = [] };
var history = await repository.GetAxBatchHistoryAsync(instanceId, now.AddDays(-90), cancellationToken).ConfigureAwait(false);
return stored with
{
History = history,
RecentRunKeys = history.ScopedRunKeys().ToHashSet(StringComparer.Ordinal),
FirstSeenExecuting = stored.Databases
.SelectMany(d => d.Jobs.Where(j => j.FirstSeenExecutingUtc is not null)
.Select(j => (Key: Core.DynamicsAx.AxBatchHistory.Key(d.Database, "#" + j.RecId.ToString(System.Globalization.CultureInfo.InvariantCulture)), At: j.FirstSeenExecutingUtc!.Value)))
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.First().At, StringComparer.OrdinalIgnoreCase),
};
}
if (collector is Collectors.DynamicsAx.AxSequenceCollector sequences)
{
var stored = await repository.GetSnapshotAsync<Core.DynamicsAx.AxSequenceSnapshot>(instanceId, collector.Name, cancellationToken).ConfigureAwait(false)
?? new Core.DynamicsAx.AxSequenceSnapshot { CapturedAt = now, Thresholds = new(), Databases = [] };
return stored with { History = await repository.GetAxSequenceHistoryAsync(instanceId, now.AddDays(-sequences.HistoryDays), cancellationToken).ConfigureAwait(false) };
}
if (collector is not Collectors.Concurrency.DeadlockCollector)
{
return null;

View file

@ -81,7 +81,7 @@ public sealed partial class MonitoringService(
private InstanceMonitor CreateMonitor(MonitoredInstance instance, AgentOptions settings, IReadOnlyDictionary<string, TimeSpan> intervals) =>
new(instance, BuiltInCollectors.Create(customRules, new CollectorSettings(groupResolver, settings.Privileges.MaxSysadminAccounts, settings.TopSql.ActivityThresholdPercent, settings.ErrorLog.LoginFailureBurst,
settings.Activity.TransactionMinutes, settings.Activity.TransactionLogMegabytes, settings.Activity.RequestMinutes,
settings.Tempdb.VersionStoreMegabytes, settings.Tempdb.SessionMegabytes, settings.DynamicsAx.ToThresholds())), intervals, connectionFactory, ruleEngine, repository,
settings.Tempdb.VersionStoreMegabytes, settings.Tempdb.SessionMegabytes)), intervals, connectionFactory, ruleEngine, repository,
loggerFactory.CreateLogger($"SQLInsight.Instance.{instance.Name}"), notifications, notificationOptions.Value.WebUrl, hubOutbox);
/// <summary>

View file

@ -116,8 +116,6 @@ builder.Services.AddSingleton<SQLInsight.Agent.Configuration.RepositoryReadiness
builder.Services.AddSingleton<SQLInsight.Agent.Configuration.InstanceConfigurationStore>();
builder.Services.AddHostedService<SQLInsight.Agent.Configuration.AgentRequestService>();
builder.Services.AddHostedService<MonitoringService>();
// 1.5: Windows servers (CIM, as the agent's identity) and synthetic checks.
SQLInsight.Agent.Servers.ServerServices.Add(builder.Services, builder.Configuration);
var host = builder.Build();
await host.RunAsync();

View file

@ -14,7 +14,6 @@
<ProjectReference Include="..\SQLInsight.Licensing\SQLInsight.Licensing.csproj" />
<ProjectReference Include="..\SQLInsight.Updates\SQLInsight.Updates.csproj" />
<ProjectReference Include="..\SQLInsight.Reports\SQLInsight.Reports.csproj" />
<ProjectReference Include="..\SQLInsight.Servers\SQLInsight.Servers.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />

View file

@ -1,467 +0,0 @@
using SQLInsight.Core.Collection;
using SQLInsight.Core.Servers;
using SQLInsight.Notifications;
using SQLInsight.Repository;
using SQLInsight.Rules.Engine;
using SQLInsight.Rules.Servers;
using SQLInsight.Servers;
namespace SQLInsight.Agent.Servers;
/// <summary>
/// Looks at one Windows server: reads the areas that are due over one CIM session, keeps the latest data for the rules,
/// evaluates them and stores findings. Only reads; a missing right makes that area "ikke målbar" with the exact reason
/// (logged once per change, never every round). Not thread-safe; one cycle at a time.
/// </summary>
public sealed partial class ServerMonitor(
WindowsServerRow server,
IWindowsServerSource source,
InsightRepository repository,
ServerOptions options,
ILogger logger,
NotificationDispatcher? notifications = null,
Uri? webUrl = null)
{
/// <summary>Rules are evaluated at least this often even when only performance was collected.</summary>
public static readonly TimeSpan EvaluationInterval = TimeSpan.FromMinutes(5);
private readonly CollectedData _data = new();
private readonly Dictionary<string, DateTimeOffset> _nextDue = new(StringComparer.Ordinal);
private readonly Dictionary<string, ServerAreaStatus> _areas = new(StringComparer.Ordinal);
private readonly Dictionary<string, string> _reported = new(StringComparer.Ordinal);
private ServerPerformanceRaw? _previousRaw;
private IReadOnlyList<ServerPerformanceSample> _samples = [];
private DateTimeOffset? _lastBoot;
private DateTimeOffset? _restartSeenAt;
private ServerEventSnapshot? _events;
private DateTimeOffset _nextEvaluation = DateTimeOffset.MinValue;
private bool _seeded;
private bool? _wasReachable;
private int _failures;
private DateTimeOffset _retryAt = DateTimeOffset.MinValue;
public WindowsServerRow Server { get; private set; } = server;
public int ServerId => Server.ServerId;
/// <summary>A server that did not answer is not tried again before this (backing off: 1, 2, 5, 10, then every 15 minutes).</summary>
public DateTimeOffset RetryAt => _retryAt;
/// <summary>The configuration changed in SQLInsight Web (role, watch lists): used from the next cycle.</summary>
public void Update(WindowsServerRow row) => Server = row;
public bool IsDue(DateTimeOffset now) => now >= _retryAt && ServerArea.All.Any(a => !_nextDue.TryGetValue(a, out var due) || due <= now);
/// <summary>How long to wait after the <paramref name="failures"/>th failure in a row to reach a server.</summary>
public static TimeSpan Backoff(int failures) => TimeSpan.FromMinutes(failures switch
{
<= 1 => 1,
2 => 2,
3 => 5,
4 => 10,
_ => 15,
});
/// <summary>
/// Reads the due areas over one session, within <see cref="ServerOptions.CycleTimeout"/>: what is not done by then is
/// "ikke målbar" this time (logged once), and the server is backed off like one that does not answer.
/// </summary>
public async Task RunCycleAsync(DateTimeOffset now, CancellationToken cancellationToken)
{
if (now < _retryAt)
{
return;
}
var due = ServerArea.All.Where(a => !_nextDue.TryGetValue(a, out var at) || at <= now).ToList();
if (due.Count == 0)
{
return;
}
foreach (var area in due)
{
_nextDue[area] = now + options.IntervalOf(area);
}
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
deadline.CancelAfter(options.CycleTimeout);
var pending = new List<string>(due);
try
{
await RunDueAsync(due, pending, now, deadline.Token).ConfigureAwait(false);
}
catch (Exception) when (deadline.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
// The deadline, not the agent stopping (the repository may also report it as its own error).
await CycleTimedOutAsync(pending, now, cancellationToken).ConfigureAwait(false);
}
}
private async Task RunDueAsync(IReadOnlyList<string> due, List<string> pending, DateTimeOffset now, CancellationToken cancellationToken)
{
if (!_seeded)
{
await SeedAsync(now, cancellationToken).ConfigureAwait(false);
_seeded = true;
}
IWindowsServerSession session;
try
{
session = await source.ConnectAsync(Server.Host, cancellationToken).ConfigureAwait(false);
}
catch (ServerAccessException ex)
{
pending.Clear();
await NotReachableAsync(ex, due, now, cancellationToken).ConfigureAwait(false);
return;
}
using (session)
{
_failures = 0;
_retryAt = DateTimeOffset.MinValue;
if (_wasReachable == false)
{
await NotifyAsync([NotificationFactory.Recovered(Server.Name, webUrl, now) with { Url = ServerNotifications.ServerPage(webUrl, ServerId) }], cancellationToken)
.ConfigureAwait(false);
}
_wasReachable = true;
_reported.Remove("connect");
foreach (var area in due)
{
await CollectAsync(session, area, now, cancellationToken).ConfigureAwait(false);
pending.Remove(area);
}
_reported.Remove("cycle");
if (due.Any(a => a != ServerArea.Performance) || now >= _nextEvaluation)
{
_nextEvaluation = now + EvaluationInterval;
await EvaluateAsync(now, cancellationToken).ConfigureAwait(false);
}
}
}
/// <summary>The round ran out of time: the areas not read are "ikke målbar" with the reason, and the server is backed off.</summary>
private async Task CycleTimedOutAsync(List<string> pending, DateTimeOffset now, CancellationToken cancellationToken)
{
var minutes = options.CycleTimeout.TotalMinutes.ToString("0.#", System.Globalization.CultureInfo.GetCultureInfo("da-DK"));
var reason = $"Serveren svarede ikke inden for {minutes} minutter, så indsamlingen blev afbrudt; området læses igen ved næste indsamling.";
if (Report("cycle", reason + string.Join(",", pending)))
{
LogCycleTimedOut(logger, Server.Name, options.CycleTimeout, string.Join(", ", pending));
}
if (pending.Count == 0)
{
return;
}
Failed(now);
foreach (var area in pending)
{
Unavailable(area, reason);
}
_nextEvaluation = now + EvaluationInterval;
await EvaluateAsync(now, cancellationToken).ConfigureAwait(false);
}
private void Failed(DateTimeOffset now)
{
_failures++;
_retryAt = now + Backoff(_failures);
}
/// <summary>Unreachable: recorded and notified once, and backed off. Refused by rights at the door: every area is "ikke målbar" with the reason.</summary>
private async Task NotReachableAsync(ServerAccessException ex, IReadOnlyList<string> due, DateTimeOffset now, CancellationToken cancellationToken)
{
if (Report("connect", ex.Message))
{
LogNotReachable(logger, Server.Name, ex.Message);
}
if (ex.Problem is ServerAccessProblem.Unreachable or ServerAccessProblem.Timeout)
{
// A server that does not answer costs a timeout per protocol: not every minute.
Failed(now);
}
await repository.RecordServerErrorAsync(ServerId, ex.Message, now, cancellationToken).ConfigureAwait(false);
if (ex.Problem is ServerAccessProblem.AccessDenied or ServerAccessProblem.NotSupported)
{
foreach (var area in ServerArea.All)
{
Unavailable(area, ex.Message);
}
await repository.RecordServerAreasAsync(ServerId, _areas.Values.ToList(), cancellationToken).ConfigureAwait(false);
if (due.Any(a => a != ServerArea.Performance) || now >= _nextEvaluation)
{
_nextEvaluation = now + EvaluationInterval;
await EvaluateAsync(now, cancellationToken).ConfigureAwait(false);
}
return;
}
if (_wasReachable != false)
{
await NotifyAsync([NotificationFactory.Unreachable(Server.Name, ex.Message, webUrl, now) with { Url = ServerNotifications.ServerPage(webUrl, ServerId) }],
cancellationToken).ConfigureAwait(false);
}
_wasReachable = false;
}
private async Task CollectAsync(IWindowsServerSession session, string area, DateTimeOffset now, CancellationToken cancellationToken)
{
try
{
var detail = area switch
{
ServerArea.Performance => await CollectPerformanceAsync(session, now, cancellationToken).ConfigureAwait(false),
ServerArea.Disks => await CollectDisksAsync(session, now, cancellationToken).ConfigureAwait(false),
ServerArea.Services => await CollectServicesAsync(session, now, cancellationToken).ConfigureAwait(false),
ServerArea.Events => await CollectEventsAsync(session, now, cancellationToken).ConfigureAwait(false),
ServerArea.Tasks => await CollectTasksAsync(session, now, cancellationToken).ConfigureAwait(false),
_ => null,
};
_areas[area] = new ServerAreaStatus(area, true, detail, ServerArea.Right(area));
if (_reported.Remove(area))
{
LogAreaRecovered(logger, Server.Name, area);
}
}
catch (ServerAccessException ex)
{
Unavailable(area, ex.Message);
if (Report(area, ex.Message))
{
LogAreaUnavailable(logger, Server.Name, area, ex.Message);
}
}
}
private void Unavailable(string area, string reason)
{
_areas[area] = new ServerAreaStatus(area, false, reason, ServerArea.Right(area));
switch (area)
{
case ServerArea.Performance:
_data.MarkUnavailable<ServerPerformanceSnapshot>(reason);
break;
case ServerArea.Disks:
_data.MarkUnavailable<ServerDiskSnapshot>(reason);
break;
case ServerArea.Services:
_data.MarkUnavailable<ServerServiceSnapshot>(reason);
break;
case ServerArea.Events:
_data.MarkUnavailable<ServerEventSnapshot>(reason);
break;
case ServerArea.Tasks:
_data.MarkUnavailable<ServerTaskSnapshot>(reason);
break;
}
}
/// <summary>Whether a problem is new (or changed) and should be logged: the same problem every minute is not news.</summary>
private bool Report(string key, string message)
{
if (_reported.TryGetValue(key, out var previous) && previous == message)
{
return false;
}
_reported[key] = message;
return true;
}
public async Task<string?> CollectPerformanceAsync(IWindowsServerSession session, DateTimeOffset now, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
var os = await session.GetOperatingSystemAsync(cancellationToken).ConfigureAwait(false);
var raw = await session.GetPerformanceAsync(now, cancellationToken).ConfigureAwait(false);
DateTimeOffset? previousBoot = null;
if (os.LastBootUpTime is { } boot && _lastBoot is { } before && Math.Abs((boot - before).TotalMinutes) > 2)
{
// Restarted since the last look: the raw counters started over too.
previousBoot = before;
_restartSeenAt = now;
_previousRaw = null;
}
_lastBoot = os.LastBootUpTime ?? _lastBoot;
var sample = ServerPerformanceMath.Sample(_previousRaw, raw, os, options.IntervalOf(ServerArea.Performance));
_previousRaw = raw;
_samples = ServerPerformanceMath.Append(_samples, sample);
var snapshot = new ServerPerformanceSnapshot
{
CapturedAt = now,
Os = os,
Raw = raw,
Samples = _samples,
PreviousBootUpTime = previousBoot,
RestartSeenAt = _restartSeenAt,
Protocol = session.Protocol,
};
_data.Set(snapshot);
await repository.SaveServerPerformanceAsync(ServerId, sample, cancellationToken).ConfigureAwait(false);
await repository.RecordServerContactAsync(ServerId, os, session.Protocol, null, now, cancellationToken).ConfigureAwait(false);
await repository.SaveServerSnapshotAsync(ServerId, ServerArea.Performance, snapshot with { Samples = [] }, now, cancellationToken).ConfigureAwait(false);
return sample.CpuPercent is { } cpu ? $"CPU {cpu:0} %" : null;
}
public async Task<string?> CollectDisksAsync(IWindowsServerSession session, DateTimeOffset now, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
var disks = await session.GetDisksAsync(cancellationToken).ConfigureAwait(false);
await repository.SaveServerDisksAsync(ServerId, now, disks, cancellationToken).ConfigureAwait(false);
var historyDays = Math.Max(1, options.Disks.ForecastHistoryDays);
var history = await repository.GetServerDiskHistoryAsync(ServerId, now.AddDays(-historyDays), cancellationToken).ConfigureAwait(false);
var settings = new DiskForecastSettings { HistoryDays = historyDays };
var forecasts = disks.Select(d => DiskForecaster.Forecast(d.Volume, history.TryGetValue(d.Volume, out var points) ? points : [], now, settings)).ToList();
var snapshot = new ServerDiskSnapshot { CapturedAt = now, Disks = disks, Forecasts = forecasts };
_data.Set(snapshot);
await repository.SaveServerSnapshotAsync(ServerId, ServerArea.Disks, snapshot, now, cancellationToken).ConfigureAwait(false);
return $"{disks.Count} drev";
}
public async Task<string?> CollectServicesAsync(IWindowsServerSession session, DateTimeOffset now, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
var services = await session.GetServicesAsync(cancellationToken).ConfigureAwait(false);
var snapshot = new ServerServiceSnapshot { CapturedAt = now, Services = services };
_data.Set(snapshot);
await repository.SaveServerSnapshotAsync(ServerId, ServerArea.Services, snapshot, now, cancellationToken).ConfigureAwait(false);
return $"{services.Count} tjenester";
}
/// <summary>
/// The first time, the last day is read, so recent problems show at once; then each log from where its last read
/// stopped, so a log that had more than could be read loses nothing while the other log keeps up.
/// </summary>
public async Task<string?> CollectEventsAsync(IWindowsServerSession session, DateTimeOffset now, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
var max = Math.Max(10, options.EventLog.MaxEventsPerCollection);
var since = ServerEventAnalysis.Logs.ToDictionary(log => log, log => EventsReadUntil(_events, log, now), StringComparer.OrdinalIgnoreCase);
var read = await session.GetEventsAsync(since, max, cancellationToken).ConfigureAwait(false);
var (groups, ignored) = ServerEventAnalysis.Group(read.Entries, ServerEventAnalysis.DefaultIgnore.Concat(options.EventLog.Ignore));
var logReadUntil = NextReadUntil(read, since, now);
var truncated = ServerEventAnalysis.Logs.Where(read.IsTruncated).ToList();
var coverage = new List<string>();
if (truncated.Count > 0)
{
coverage.Add($"Der var flere end {max} hændelser i {string.Join(" og ", truncated)} siden sidst; resten læses ved næste indsamling.");
}
if (read.Note is { } note)
{
coverage.Add(note);
}
var snapshot = new ServerEventSnapshot
{
CapturedAt = now,
ReadUntil = logReadUntil.Values.Min(),
LogReadUntil = logReadUntil,
Method = read.Method,
Groups = groups,
Recent = ServerEventAnalysis.Recent(_events?.Recent ?? [], groups, now),
Ignored = ignored,
Coverage = coverage.Count == 0 ? null : string.Join(" ", coverage),
};
_events = snapshot;
_data.Set(snapshot);
await repository.SaveServerEventsAsync(ServerId, snapshot, cancellationToken).ConfigureAwait(false);
await repository.SaveServerSnapshotAsync(ServerId, ServerArea.Events, snapshot with { Groups = [] }, now, cancellationToken).ConfigureAwait(false);
return $"{read.Entries.Count} hændelser ({read.Method})";
}
/// <summary>Where a log's next read starts: its own mark, else the snapshot's (from before per-log marks), else a day back.</summary>
public static DateTimeOffset EventsReadUntil(ServerEventSnapshot? previous, string log, DateTimeOffset now) =>
previous?.LogReadUntil?.FirstOrDefault(m => string.Equals(m.Key, log, StringComparison.OrdinalIgnoreCase)) is { Key: not null } mark ? mark.Value
: previous?.ReadUntil ?? now - ServerEventAnalysis.RecentWindow;
/// <summary>
/// Per log: strictly after the newest event read (for a log that had more, that is where its rest starts); without
/// events, ten minutes' margin for events written late.
/// </summary>
public static Dictionary<string, DateTimeOffset> NextReadUntil(ServerEventRead read, IReadOnlyDictionary<string, DateTimeOffset> since, DateTimeOffset now) =>
ServerEventAnalysis.Logs.ToDictionary(log => log, log =>
{
var from = since[log];
var newest = read.Entries.Where(e => string.Equals(e.Log, log, StringComparison.OrdinalIgnoreCase)).Select(e => (DateTimeOffset?)e.At).Max();
return newest is { } at ? Max(at, from) : Max(from, now.AddMinutes(-10));
}, StringComparer.Ordinal);
private static DateTimeOffset Max(DateTimeOffset a, DateTimeOffset b) => a > b ? a : b;
public async Task<string?> CollectTasksAsync(IWindowsServerSession session, DateTimeOffset now, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
var tasks = options.Tasks;
var read = await session.GetTasksAsync(
tasks.Watches,
Math.Max(1, tasks.MaxWatched), options.TaskBudget, cancellationToken).ConfigureAwait(false);
var snapshot = new ServerTaskSnapshot { CapturedAt = now, Tasks = read.Watched, Visible = read.Visible, Coverage = read.Note };
_data.Set(snapshot);
await repository.SaveServerSnapshotAsync(ServerId, ServerArea.Tasks, snapshot, now, cancellationToken).ConfigureAwait(false);
return $"{read.Watched.Count} af {read.Visible} opgaver overvåges";
}
private async Task EvaluateAsync(DateTimeOffset now, CancellationToken cancellationToken)
{
await repository.RecordServerAreasAsync(ServerId, ServerArea.All.Where(_areas.ContainsKey).Select(a => _areas[a]).ToList(), cancellationToken).ConfigureAwait(false);
var context = new ServerRuleContext(Server.ToIdentity(), now, _data, options) { PerformanceInterval = options.IntervalOf(ServerArea.Performance) };
var results = ScopedRuleEngine.Evaluate(WindowsServerRules.Create(), context);
var changes = await repository.SaveScopedRuleResultsAsync(FindingScope.Server, ServerId, results, now, cancellationToken).ConfigureAwait(false);
await NotifyAsync(ServerNotifications.For(Server.Name, changes, webUrl, now).ToList(), cancellationToken).ConfigureAwait(false);
if (logger.IsEnabled(LogLevel.Debug))
{
var findings = results.Sum(r => r.Findings.Count);
var notMeasurable = results.Count(r => r.Outcome is RuleOutcome.NotMeasurable or RuleOutcome.Error);
LogCycle(logger, Server.Name, findings, notMeasurable);
}
}
/// <summary>What a restart of the agent would lose: the last hour's samples, the boot time and where the event log was read to.</summary>
private async Task SeedAsync(DateTimeOffset now, CancellationToken cancellationToken)
{
var stored = await repository.GetServerPerformanceAsync(ServerId, now - ServerPerformanceMath.Window, cancellationToken).ConfigureAwait(false);
_samples = stored.Select(s => new ServerPerformanceSample(new DateTimeOffset(DateTime.SpecifyKind(s.SampledAt, DateTimeKind.Utc)), s.CpuPercent, s.AvailableMb,
s.AvailableMb is { } mb && s.AvailablePct is > 0 ? mb * 100 / s.AvailablePct.Value : null, s.PagesPerSec, s.CommitPct)).ToList();
var performance = await repository.GetServerSnapshotAsync<ServerPerformanceSnapshot>(ServerId, ServerArea.Performance, cancellationToken).ConfigureAwait(false);
_lastBoot = performance?.Os.LastBootUpTime;
_restartSeenAt = performance?.RestartSeenAt;
_events = await repository.GetServerSnapshotAsync<ServerEventSnapshot>(ServerId, ServerArea.Events, cancellationToken).ConfigureAwait(false);
}
private async Task NotifyAsync(List<Notification> events, CancellationToken cancellationToken)
{
if (notifications is not null && events.Count > 0)
{
await notifications.DispatchAsync(events, cancellationToken).ConfigureAwait(false);
}
}
[LoggerMessage(Level = LogLevel.Warning, Message = "Windows server {Server} did not finish within {Timeout}; not read this time: {Areas}")]
private static partial void LogCycleTimedOut(ILogger logger, string server, TimeSpan timeout, string areas);
[LoggerMessage(Level = LogLevel.Warning, Message = "Windows server {Server} cannot be read: {Reason}")]
private static partial void LogNotReachable(ILogger logger, string server, string reason);
[LoggerMessage(Level = LogLevel.Information, Message = "Windows server {Server}: {Area} is not measurable: {Reason}")]
private static partial void LogAreaUnavailable(ILogger logger, string server, string area, string reason);
[LoggerMessage(Level = LogLevel.Information, Message = "Windows server {Server}: {Area} can be read again")]
private static partial void LogAreaRecovered(ILogger logger, string server, string area);
[LoggerMessage(Level = LogLevel.Debug, Message = "Windows server {Server}: {Findings} findings, {NotMeasurable} rules not measurable")]
private static partial void LogCycle(ILogger logger, string server, int findings, int notMeasurable);
}

View file

@ -1,231 +0,0 @@
using Microsoft.Extensions.Options;
using SQLInsight.Core.Servers;
using SQLInsight.Notifications;
using SQLInsight.Repository;
using SQLInsight.Servers;
namespace SQLInsight.Agent.Servers;
/// <summary>
/// The agent's loop for Windows servers: keeps the SQL Server hosts in step with the monitored instances, reads the
/// configured servers (changes in SQLInsight Web apply within a minute, without a restart), runs each server's due
/// areas with a limit on how many at once, and deletes old samples and check results hourly (also when only the
/// synthetic checks are on).
/// </summary>
public sealed partial class ServerMonitoringService(
IOptions<ServerOptions> options,
IOptions<SyntheticCheckOptions> checkOptions,
IWindowsServerSource source,
InsightRepository repository,
TimeProvider time,
ILoggerFactory loggerFactory,
NotificationDispatcher notifications,
IOptions<NotificationOptions> notificationOptions,
IOptions<AgentOptions> agentOptions,
Configuration.RepositoryReadiness readiness) : BackgroundService
{
public static readonly TimeSpan RefreshInterval = TimeSpan.FromMinutes(1);
public static readonly TimeSpan MaintenanceInterval = TimeSpan.FromHours(1);
private readonly ILogger _logger = loggerFactory.CreateLogger<ServerMonitoringService>();
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var settings = options.Value;
if (!settings.Enabled)
{
LogDisabled(_logger);
if (checkOptions.Value.Enabled)
{
// The synthetic checks keep writing results: their retention still applies.
await MaintainOnlyAsync(settings, stoppingToken).ConfigureAwait(false);
}
return;
}
await readiness.Ready.WaitAsync(stoppingToken).ConfigureAwait(false);
var monitors = new Dictionary<int, ServerMonitor>();
var running = new Dictionary<int, Task>();
using var throttle = new SemaphoreSlim(Math.Clamp(settings.MaxParallelServers, 1, 32));
var nextRefresh = DateTimeOffset.MinValue;
var nextMaintenance = time.GetUtcNow().AddMinutes(5);
using var timer = new PeriodicTimer(agentOptions.Value.Tick, time);
try
{
do
{
var now = time.GetUtcNow();
try
{
if (now >= nextRefresh)
{
nextRefresh = now + RefreshInterval;
await RefreshAsync(monitors, settings, now, stoppingToken).ConfigureAwait(false);
}
foreach (var (id, monitor) in monitors)
{
if (running.TryGetValue(id, out var task) && !task.IsCompleted || !monitor.IsDue(now))
{
continue;
}
running[id] = RunThrottledAsync(monitor, throttle, stoppingToken);
}
foreach (var done in running.Where(r => r.Value.IsCompleted && !monitors.ContainsKey(r.Key)).Select(r => r.Key).ToList())
{
running.Remove(done);
}
if (now >= nextMaintenance)
{
nextMaintenance = now + MaintenanceInterval;
await MaintainAsync(settings, now, stoppingToken).ConfigureAwait(false);
}
}
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
{
// Typically the repository is unreachable. Try again next round.
LogRoundFailed(_logger, ex);
}
}
while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false));
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Stopping: the running cycles end below, before the throttle they release is disposed.
}
await WaitQuietlyAsync(running.Values).ConfigureAwait(false);
}
/// <summary>Servers off, synthetic checks on: only the hourly maintenance.</summary>
private async Task MaintainOnlyAsync(ServerOptions settings, CancellationToken stoppingToken)
{
try
{
await readiness.Ready.WaitAsync(stoppingToken).ConfigureAwait(false);
using var timer = new PeriodicTimer(MaintenanceInterval, time);
do
{
try
{
await MaintainAsync(settings, time.GetUtcNow(), stoppingToken).ConfigureAwait(false);
}
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
{
LogRoundFailed(_logger, ex);
}
}
while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false));
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Stopping.
}
}
/// <summary>Deletes what is past retention, in small batches; every period is at least a day (see <see cref="ServerOptions.Normalize"/>).</summary>
private Task MaintainAsync(ServerOptions settings, DateTimeOffset now, CancellationToken cancellationToken) =>
repository.RunServerMaintenanceAsync(new ServerRetention
{
SampleDays = Math.Max(1, settings.SampleDays),
DiskHistoryDays = Math.Max(Math.Max(1, settings.DiskHistoryDays), settings.Disks.ForecastHistoryDays + 1),
EventDays = Math.Max(1, settings.EventDays),
CheckResultDays = Math.Max(1, checkOptions.Value.ResultDays),
ResolvedFindingDays = Math.Max(1, agentOptions.Value.Retention.ResolvedFindingDays),
}, now, cancellationToken);
/// <summary>
/// Waits, when the agent stops, for tasks that end with it: a cancellation (or the error a cancelled database call
/// reports instead) is how they end, not news.
/// </summary>
internal static async Task WaitQuietlyAsync(IEnumerable<Task> tasks)
{
foreach (var task in tasks)
{
try
{
await task.ConfigureAwait(false);
}
catch (Exception ex) when (ex is OperationCanceledException or System.Data.Common.DbException)
{
// Stopping.
}
}
}
/// <summary>The SQL Server hosts first, then the servers to look at (not paused).</summary>
private async Task RefreshAsync(Dictionary<int, ServerMonitor> monitors, ServerOptions settings, DateTimeOffset now, CancellationToken cancellationToken)
{
if (settings.IncludeSqlHosts)
{
var hosts = (await repository.GetConfiguredInstancesAsync(cancellationToken).ConfigureAwait(false))
.Select(i => ServerHost.FromDataSource(i.DataSource, Environment.MachineName).Host)
.OfType<string>()
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(h => (h, ServerHost.ShortName(h)))
.ToList();
await repository.SyncSqlHostServersAsync(hosts, now, cancellationToken).ConfigureAwait(false);
}
var servers = (await repository.GetWindowsServersAsync(cancellationToken).ConfigureAwait(false)).Where(s => !s.Paused).ToList();
foreach (var row in servers)
{
if (monitors.TryGetValue(row.ServerId, out var monitor) && string.Equals(monitor.Server.Host, row.Host, StringComparison.OrdinalIgnoreCase))
{
monitor.Update(row);
continue;
}
monitors[row.ServerId] = new ServerMonitor(row, source, repository, settings, loggerFactory.CreateLogger($"SQLInsight.Server.{row.Name}"), notifications,
notificationOptions.Value.WebUrl);
LogStarted(_logger, row.Name, row.Host);
}
foreach (var gone in monitors.Keys.Where(id => servers.All(s => s.ServerId != id)).ToList())
{
LogStopped(_logger, monitors[gone].Server.Name);
monitors.Remove(gone);
}
}
/// <summary>
/// One server's cycle when a slot is free, with the time taken then (not when it was queued). The cycle bounds
/// itself (<see cref="ServerOptions.CycleTimeout"/>), so a hung server gives its slot back.
/// </summary>
private async Task RunThrottledAsync(ServerMonitor monitor, SemaphoreSlim throttle, CancellationToken cancellationToken)
{
await throttle.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
await monitor.RunCycleAsync(time.GetUtcNow(), cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
{
LogCycleFailed(_logger, ex, monitor.Server.Name);
}
finally
{
throttle.Release();
}
}
[LoggerMessage(Level = LogLevel.Information, Message = "Windows servers are not monitored (SQLInsight:Servers:Enabled is false).")]
private static partial void LogDisabled(ILogger logger);
[LoggerMessage(Level = LogLevel.Information, Message = "Windows server {Server} ({Host}): monitoring started")]
private static partial void LogStarted(ILogger logger, string server, string host);
[LoggerMessage(Level = LogLevel.Information, Message = "Windows server {Server}: monitoring stopped (paused or removed)")]
private static partial void LogStopped(ILogger logger, string server);
[LoggerMessage(Level = LogLevel.Error, Message = "Windows server {Server}: cycle failed")]
private static partial void LogCycleFailed(ILogger logger, Exception exception, string server);
[LoggerMessage(Level = LogLevel.Error, Message = "Monitoring Windows servers failed this round; retrying.")]
private static partial void LogRoundFailed(ILogger logger, Exception exception);
}

View file

@ -1,139 +0,0 @@
using SQLInsight.Core.Servers;
using SQLInsight.Core.SyntheticChecks;
using SQLInsight.Notifications;
using SQLInsight.Repository;
using SQLInsight.Rules.Servers;
using SQLInsight.Servers;
namespace SQLInsight.Agent.Servers;
/// <summary>Windows servers (SQLInsight:Servers): how and how often to collect, how long to keep it, and the rules' thresholds.</summary>
public sealed class ServerOptions : ServerRuleSettings
{
public const string SectionName = "SQLInsight:Servers";
public static readonly TimeSpan MinCycleTimeout = TimeSpan.FromMinutes(1);
public static readonly TimeSpan MaxCycleTimeout = TimeSpan.FromMinutes(30);
/// <summary>Look at Windows servers at all.</summary>
public bool Enabled { get; set; } = true;
/// <summary>The computers behind the monitored SQL Server instances are Windows servers too, without being added.</summary>
public bool IncludeSqlHosts { get; set; } = true;
/// <summary>How many servers are read at the same time (1 to 32).</summary>
public int MaxParallelServers { get; set; } = 4;
/// <summary>Auto (WS-Man, then DCOM), WSMan or DCOM.</summary>
public string Protocol { get; set; } = "Auto";
/// <summary>How long one CIM operation may take (5 seconds to 5 minutes).</summary>
public TimeSpan OperationTimeout { get; set; } = TimeSpan.FromSeconds(30);
/// <summary>
/// How long one server's round may take at most (1 to 30 minutes, at least twice <see cref="OperationTimeout"/>): what is
/// not read by then is "ikke målbar" until the next round, so a server that hangs never holds up the others.
/// </summary>
public TimeSpan CycleTimeout { get; set; } = TimeSpan.FromMinutes(5);
/// <summary>Read event logs with the Event Log service's own protocol (filters on the server); off: CIM only.</summary>
public bool EventLogRpc { get; set; } = true;
/// <summary>Overrides of the collection intervals by area: performance, disks, services, events, tasks.</summary>
public IDictionary<string, TimeSpan> Intervals { get; } = new Dictionary<string, TimeSpan>(StringComparer.OrdinalIgnoreCase);
/// <summary>Per-minute CPU and memory samples (at least 1 day).</summary>
public int SampleDays { get; set; } = 14;
/// <summary>Free space per volume: at least the forecast's history.</summary>
public int DiskHistoryDays { get; set; } = 35;
/// <summary>Grouped Windows events (at least 1 day).</summary>
public int EventDays { get; set; } = 90;
/// <summary>The share of <see cref="CycleTimeout"/> the scheduled tasks' run information may take; the rest is read next time.</summary>
public TimeSpan TaskBudget => CycleTimeout / 3;
public TimeSpan IntervalOf(string area) =>
Intervals.TryGetValue(area, out var interval) && interval >= TimeSpan.FromSeconds(30) ? interval : ServerArea.DefaultInterval(area);
public ServerConnectionOptions Connection => new() { Protocol = Protocol, OperationTimeout = OperationTimeout, EventLogRpc = EventLogRpc };
/// <summary>
/// Keeps the settings within what works: a timeout of 0 or hours, retention of 0 days (maintenance would delete
/// everything) or unbounded parallelism are moved to the nearest sensible value. Applied when the options are bound.
/// </summary>
public ServerOptions Normalize()
{
MaxParallelServers = Math.Clamp(MaxParallelServers, 1, 32);
OperationTimeout = ServerConnectionOptions.BoundTimeout(OperationTimeout);
var cycle = CycleTimeout < OperationTimeout * 2 ? OperationTimeout * 2 : CycleTimeout;
CycleTimeout = cycle < MinCycleTimeout ? MinCycleTimeout : cycle > MaxCycleTimeout ? MaxCycleTimeout : cycle;
SampleDays = Math.Max(1, SampleDays);
DiskHistoryDays = Math.Max(1, DiskHistoryDays);
EventDays = Math.Max(1, EventDays);
Disks.ForecastHistoryDays = Math.Clamp(Disks.ForecastHistoryDays, 1, 365);
EventLog.MaxEventsPerCollection = Math.Clamp(EventLog.MaxEventsPerCollection, 10, 10000);
Tasks.MaxWatched = Math.Clamp(Tasks.MaxWatched, 1, 200);
return this;
}
}
/// <summary>Synthetic checks (SQLInsight:SyntheticChecks): how many run at once, how long results are kept, and the rules' thresholds.</summary>
public sealed class SyntheticCheckOptions : SyntheticCheckRuleSettings
{
public const string SectionName = "SQLInsight:SyntheticChecks";
/// <summary>The most recent results the rules look at: enough for "fejlet i træk", never unbounded.</summary>
public const int MaxRecentResults = 100;
public bool Enabled { get; set; } = true;
/// <summary>How many checks run at the same time (1 to 32).</summary>
public int MaxParallelChecks { get; set; } = 4;
/// <summary>Every run's result (at least 1 day).</summary>
public int ResultDays { get; set; } = 14;
/// <summary>Where the Dynamics AX .NET Business Connector is, when not where the AX installer puts it.</summary>
public string? BusinessConnectorPath { get; set; }
/// <summary>How many recent results the rules are given: more than they need to count failures or slow runs in a row.</summary>
public int RecentResults => Math.Clamp(Math.Max(10, Math.Max(FailuresBeforeFinding, SlowRunsBeforeFinding)) + 1, 11, MaxRecentResults);
/// <summary>Keeps the settings within what works (see <see cref="ServerOptions.Normalize"/>). Applied when the options are bound.</summary>
public SyntheticCheckOptions Normalize()
{
MaxParallelChecks = Math.Clamp(MaxParallelChecks, 1, 32);
ResultDays = Math.Max(1, ResultDays);
FailuresBeforeFinding = Math.Clamp(FailuresBeforeFinding, 1, MaxRecentResults - 1);
SlowRunsBeforeFinding = Math.Clamp(SlowRunsBeforeFinding, 1, MaxRecentResults - 1);
return this;
}
/// <summary>A check's interval as the agent runs it: never more often than <see cref="SyntheticCheckLimits.MinIntervalSeconds"/>.</summary>
public static TimeSpan IntervalOf(int intervalSeconds) =>
TimeSpan.FromSeconds(Math.Clamp(intervalSeconds, SyntheticCheckLimits.MinIntervalSeconds, SyntheticCheckLimits.MaxIntervalSeconds));
}
/// <summary>Notifications about Windows servers and synthetic checks, with links to their pages in the web UI.</summary>
internal static class ServerNotifications
{
public static IEnumerable<Notification> For(string name, FindingChanges changes, Uri? webUrl, DateTimeOffset now)
{
foreach (var (id, finding) in changes.Opened)
{
yield return NotificationFactory.Opened(name, id, finding, webUrl, now) with { Url = Link(webUrl, id) };
}
foreach (var resolved in changes.Resolved)
{
yield return NotificationFactory.Resolved(name, resolved.FindingId, resolved.RuleId, resolved.Title, (Core.Findings.Severity)resolved.Severity, webUrl, now)
with { Url = Link(webUrl, resolved.FindingId) };
}
}
public static Uri? Link(Uri? webUrl, long findingId) => webUrl is null ? null : new Uri(webUrl, $"servere/fund/{findingId}");
public static Uri? ServerPage(Uri? webUrl, int serverId) => webUrl is null ? null : new Uri(webUrl, $"servere/{serverId}");
}

View file

@ -1,65 +0,0 @@
using SQLInsight.Core.Configuration;
using SQLInsight.Core.Servers;
using SQLInsight.Core.SyntheticChecks;
using SQLInsight.Repository;
using SQLInsight.Servers;
namespace SQLInsight.Agent.Servers;
/// <summary>
/// Requests from SQLInsight Web about Windows servers and synthetic checks (insight.AgentRequests with a TargetId):
/// test a server's rights as the agent, or run a check now. Called by the agent's request service.
/// </summary>
public sealed class ServerRequestHandler(InsightRepository repository, IWindowsServerSource source, SyntheticCheckService checks, TimeProvider time)
{
/// <summary>Margin for reading the request and storing its result.</summary>
public static readonly TimeSpan Margin = TimeSpan.FromSeconds(30);
/// <summary>
/// How long such a request may take: a check's longest timeout, the lookup of its Windows service and storing the
/// result, so "Kør nu" always gets its result stored.
/// </summary>
public static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(SyntheticCheckLimits.MaxTimeoutSeconds) + SyntheticCheckService.ServiceLookupTimeout + Margin;
/// <summary>How long a server test may take: what is not done by then is reported as not answered, in time to be stored.</summary>
public static readonly TimeSpan TestTimeout = RequestTimeout - Margin;
public static bool Handles(string kind) => ServerRequestKind.Handles(kind);
/// <summary>The request's result as JSON. Throws <see cref="KeyNotFoundException"/> for a server or check that does not exist.</summary>
public async Task<string> HandleAsync(AgentRequestRow request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var target = await repository.GetRequestTargetAsync(request.RequestId, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Anmodningen mangler en server eller et tjek.");
return request.Kind switch
{
ServerRequestKind.ServerTest => AgentRequestJson.Serialize(await TestAsync(target, cancellationToken).ConfigureAwait(false)),
ServerRequestKind.SyntheticCheckRun => AgentRequestJson.Serialize(await RunAsync(target, cancellationToken).ConfigureAwait(false)),
_ => throw new InvalidOperationException($"Ukendt anmodning: {request.Kind}."),
};
}
/// <summary>Connects as the agent and tries every area; what could and could not be read is also stored on the server.</summary>
public async Task<WindowsServerTestResult> TestAsync(int serverId, CancellationToken cancellationToken)
{
var server = await repository.GetWindowsServerAsync(serverId, cancellationToken).ConfigureAwait(false)
?? throw new KeyNotFoundException($"Server {serverId} findes ikke.");
var account = OperatingSystem.IsWindows() ? CimWindowsServerSource.AgentAccount(server.Host) : null;
var result = await WindowsServerTester.TestAsync(source, server.Host, account, time.GetUtcNow(), TestTimeout, cancellationToken).ConfigureAwait(false);
if (result.Reachable)
{
await repository.RecordServerAreasAsync(serverId, result.Areas, cancellationToken).ConfigureAwait(false);
}
return result;
}
/// <summary>"Kør nu": joins a run of the check that is going on, or runs it within the limit on parallel checks; a paused check is not run.</summary>
public async Task<SyntheticCheckResult> RunAsync(int checkId, CancellationToken cancellationToken)
{
var check = await repository.GetSyntheticCheckAsync(checkId, cancellationToken).ConfigureAwait(false)
?? throw new KeyNotFoundException($"Tjek {checkId} findes ikke.");
return await checks.RunNowAsync(check, cancellationToken).ConfigureAwait(false);
}
}

View file

@ -1,31 +0,0 @@
using Microsoft.Extensions.Options;
using SQLInsight.Connectivity;
using SQLInsight.Servers;
using SQLInsight.Servers.Synthetic;
namespace SQLInsight.Agent.Servers;
/// <summary>1.5: Windows servers and synthetic checks in the agent.</summary>
public static class ServerServices
{
public static void Add(IServiceCollection services, IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);
services.Configure<ServerOptions>(configuration.GetSection(ServerOptions.SectionName));
services.Configure<SyntheticCheckOptions>(configuration.GetSection(SyntheticCheckOptions.SectionName));
services.PostConfigure<ServerOptions>(o => o.Normalize());
services.PostConfigure<SyntheticCheckOptions>(o => o.Normalize());
services.AddSingleton<IWindowsServerSource>(sp => OperatingSystem.IsWindows()
? new CimWindowsServerSource(sp.GetRequiredService<IOptions<ServerOptions>>().Value.Connection, sp.GetRequiredService<TimeProvider>())
: new UnsupportedWindowsServerSource());
services.AddSingleton<IBusinessConnectorLocator>(sp => new BusinessConnectorLocator(sp.GetRequiredService<IOptions<SyntheticCheckOptions>>().Value.BusinessConnectorPath));
services.AddSingleton<IProcessRunner, ProcessRunner>();
services.AddSingleton(sp => new SyntheticCheckRunner(sp.GetRequiredService<ISqlConnectionFactory>(), sp.GetRequiredService<IBusinessConnectorLocator>(),
sp.GetRequiredService<IProcessRunner>(), sp.GetRequiredService<TimeProvider>()));
services.AddSingleton<SyntheticCheckService>();
services.AddHostedService(sp => sp.GetRequiredService<SyntheticCheckService>());
services.AddSingleton<ServerRequestHandler>();
services.AddHostedService<ServerMonitoringService>();
}
}

View file

@ -1,367 +0,0 @@
using Microsoft.Extensions.Options;
using SQLInsight.Core.Configuration;
using SQLInsight.Core.SyntheticChecks;
using SQLInsight.Notifications;
using SQLInsight.Repository;
using SQLInsight.Rules.Servers;
using SQLInsight.Servers;
using SQLInsight.Servers.Synthetic;
namespace SQLInsight.Agent.Servers;
/// <summary>
/// Runs the synthetic checks on their schedule (and on request from SQLInsight Web), as the agent's identity: stores
/// each result, looks up the linked Windows service when a check fails ("svarer ikke, selv om tjenesten kører"),
/// evaluates the rules and notifies. A check runs once at a time, and at most MaxParallelChecks run together, also when
/// asked to run now. Never changes anything on the servers it checks.
/// </summary>
public sealed partial class SyntheticCheckService(
IOptions<SyntheticCheckOptions> options,
SyntheticCheckRunner runner,
IWindowsServerSource servers,
InsightRepository repository,
Configuration.AgentCredentialKey key,
TimeProvider time,
ILogger<SyntheticCheckService> logger,
NotificationDispatcher notifications,
IOptions<NotificationOptions> notificationOptions,
IOptions<AgentOptions> agentOptions,
Configuration.RepositoryReadiness readiness) : BackgroundService
{
/// <summary>The answer to "Kør nu" for a paused check: it is not run.</summary>
public const string PausedMessage = "Tjekket er sat på pause.";
/// <summary>How long looking up the state of a failing check's Windows service may take.</summary>
public static readonly TimeSpan ServiceLookupTimeout = TimeSpan.FromSeconds(30);
private readonly Dictionary<int, Task<SyntheticCheckResult?>> _running = [];
private readonly Dictionary<int, DateTimeOffset> _attempts = [];
private readonly Dictionary<int, string> _reported = [];
private readonly Lock _lock = new();
private readonly SemaphoreSlim _throttle = new(Math.Clamp(options.Value.MaxParallelChecks, 1, 32));
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var settings = options.Value;
if (!settings.Enabled)
{
return;
}
await readiness.Ready.WaitAsync(stoppingToken).ConfigureAwait(false);
using var timer = new PeriodicTimer(agentOptions.Value.Tick, time);
try
{
do
{
try
{
var now = time.GetUtcNow();
var checks = await repository.GetSyntheticChecksAsync(stoppingToken).ConfigureAwait(false);
Prune(checks);
foreach (var check in checks.Where(c => IsDue(c, now)))
{
lock (_lock)
{
_attempts[check.CheckId] = now;
}
_ = Start(check, stoppingToken);
}
}
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
{
LogRoundFailed(logger, ex);
}
}
while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false));
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Stopping: the running checks end below.
}
Task[] remaining;
lock (_lock)
{
remaining = [.. _running.Values];
}
await ServerMonitoringService.WaitQuietlyAsync(remaining).ConfigureAwait(false);
}
public override void Dispose()
{
base.Dispose();
_throttle.Dispose();
}
/// <summary>
/// Due when its interval (never under a minute) has passed since the last stored result and since the agent last
/// started it: a run that could not be stored is not repeated every few seconds.
/// </summary>
private bool IsDue(SyntheticCheckRow check, DateTimeOffset now)
{
if (check.Paused)
{
return false;
}
var interval = SyntheticCheckOptions.IntervalOf(check.IntervalSeconds);
if (check.LastRunAt is { } last && new DateTimeOffset(DateTime.SpecifyKind(last, DateTimeKind.Utc)) + interval > now)
{
return false;
}
lock (_lock)
{
return !(_attempts.TryGetValue(check.CheckId, out var attempted) && attempted + interval > now)
&& !(_running.TryGetValue(check.CheckId, out var running) && !running.IsCompleted);
}
}
/// <summary>Forgets checks that were removed.</summary>
private void Prune(IReadOnlyList<SyntheticCheckRow> checks)
{
var ids = checks.Select(c => c.CheckId).ToHashSet();
lock (_lock)
{
foreach (var id in _running.Where(r => !ids.Contains(r.Key) && r.Value.IsCompleted).Select(r => r.Key).ToList())
{
_running.Remove(id);
}
foreach (var id in _attempts.Keys.Where(id => !ids.Contains(id)).ToList())
{
_attempts.Remove(id);
}
foreach (var id in _reported.Keys.Where(id => !ids.Contains(id)).ToList())
{
_reported.Remove(id);
}
}
}
/// <summary>The check's run that is going on, or a new one under the limit on parallel checks.</summary>
private Task<SyntheticCheckResult?> Start(SyntheticCheckRow check, CancellationToken cancellationToken)
{
lock (_lock)
{
if (_running.TryGetValue(check.CheckId, out var running) && !running.IsCompleted)
{
return running;
}
var task = RunThrottledAsync(check, cancellationToken);
_running[check.CheckId] = task;
return task;
}
}
/// <summary>
/// "Kør nu" from SQLInsight Web: joins the check's run if one is going on, or runs it under the same limit as the
/// scheduled runs. A paused check is not run (and nothing is stored or evaluated).
/// </summary>
public async Task<SyntheticCheckResult> RunNowAsync(SyntheticCheckRow check, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(check);
if (check.Paused)
{
return new SyntheticCheckResult { At = time.GetUtcNow(), Success = false, Message = PausedMessage };
}
var result = await Start(check, cancellationToken).WaitAsync(cancellationToken).ConfigureAwait(false);
return result ?? throw new InvalidOperationException("Tjekket kunne ikke køres eller resultatet ikke gemmes. Se agentens log.");
}
/// <summary>One run; null when it could not be stored (logged once per change).</summary>
private async Task<SyntheticCheckResult?> RunThrottledAsync(SyntheticCheckRow check, CancellationToken cancellationToken)
{
await _throttle.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
return await RunCheckAsync(check, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
{
if (IsNews(check.CheckId, "fejl: " + ex.Message, remember: true))
{
LogCheckFailed(logger, ex, check.Name);
}
return null;
}
finally
{
_throttle.Release();
}
}
/// <summary>Runs a check once, stores the result and its findings. Used on schedule and for "Kør nu" (through <see cref="RunNowAsync"/>).</summary>
public async Task<SyntheticCheckResult> RunCheckAsync(SyntheticCheckRow check, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(check);
var settings = check.Settings;
var now = time.GetUtcNow();
string? password = null;
if (check.Kind == SyntheticCheckKind.Sql && settings.Authentication == "SqlLogin")
{
(password, var error) = await OpenSecretAsync(check, settings, cancellationToken).ConfigureAwait(false);
if (error is not null)
{
return await StoreAsync(check, new SyntheticCheckResult { At = now, Success = false, Message = error }, cancellationToken).ConfigureAwait(false);
}
}
var timeout = TimeSpan.FromSeconds(Math.Clamp(check.TimeoutSeconds, 1, SyntheticCheckLimits.MaxTimeoutSeconds));
SyntheticCheckResult result;
try
{
result = await runner.RunAsync(new SyntheticCheckRun(check.Kind, settings, timeout, password), cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
{
// Not a failure the runner foresaw: still the check's result, so it is stored, waits its interval, and is logged once.
var message = "Tjekket kunne ikke køres: " + ex.Message;
if (IsNews(check.CheckId, message, remember: false))
{
LogCheckFailed(logger, ex, check.Name);
}
result = new SyntheticCheckResult { At = now, Success = false, DurationMs = (int)Math.Min(int.MaxValue, (time.GetUtcNow() - now).TotalMilliseconds), Message = message };
}
if (!result.Success && check.ServerId is { } serverId && settings.ServiceName is { Length: > 0 } service)
{
result = await WithServiceStateAsync(result, serverId, service, cancellationToken).ConfigureAwait(false);
}
return await StoreAsync(check, result, cancellationToken).ConfigureAwait(false);
}
private async Task<(string? Password, string? Error)> OpenSecretAsync(SyntheticCheckRow check, SyntheticCheckSettings settings, CancellationToken cancellationToken)
{
string? error = null;
string? password = null;
if (check.SecretCipher is not { Length: > 0 } cipher)
{
error = "Der er ikke gemt en adgangskode til SQL-login'et.";
}
else
{
try
{
password = key.OpenContext(cipher, settings.SecretContext());
}
catch (CredentialException ex)
{
error = "Adgangskoden kan ikke bruges: " + ex.Message;
}
}
if (!string.Equals(error, check.SecretError, StringComparison.Ordinal))
{
await repository.SetSyntheticCheckSecretErrorAsync(check.CheckId, error, cancellationToken).ConfigureAwait(false);
}
return (password, error);
}
/// <summary>
/// The state of the Windows service behind a failing check: the difference between "nede" and "kører, men svarer
/// ikke". Takes at most <see cref="ServiceLookupTimeout"/>.
/// </summary>
private async Task<SyntheticCheckResult> WithServiceStateAsync(SyntheticCheckResult result, int serverId, string service, CancellationToken cancellationToken)
{
var server = await repository.GetWindowsServerAsync(serverId, cancellationToken).ConfigureAwait(false);
if (server is null)
{
return result;
}
using var lookup = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
lookup.CancelAfter(ServiceLookupTimeout);
try
{
using var session = await servers.ConnectAsync(server.Host, lookup.Token).ConfigureAwait(false);
var state = await session.GetServiceAsync(service, lookup.Token).ConfigureAwait(false);
return result with
{
ServiceState = state?.State ?? "NotFound",
Details = [.. result.Details, new SyntheticCheckDetail("Tjeneste", $"{service} på {server.Name}: {state?.State ?? "findes ikke"}")],
};
}
catch (ServerAccessException ex)
{
// The reason names the agent's account: kept on-prem.
return result with { Details = [.. result.Details, new SyntheticCheckDetail("Tjeneste", $"{service} på {server.Name} kunne ikke aflæses: {ex.Message}", Sensitive: true)] };
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
return result with
{
Details = [.. result.Details, new SyntheticCheckDetail("Tjeneste",
$"{service} på {server.Name} kunne ikke aflæses: serveren svarede ikke inden for {ServiceLookupTimeout.TotalSeconds:0} sekunder.")],
};
}
}
private async Task<SyntheticCheckResult> StoreAsync(SyntheticCheckRow check, SyntheticCheckResult result, CancellationToken cancellationToken)
{
await repository.RecordSyntheticCheckResultAsync(check.CheckId, result, cancellationToken).ConfigureAwait(false);
var settings = options.Value;
var recent = (await repository.GetSyntheticCheckResultsAsync(check.CheckId, settings.RecentResults, cancellationToken: cancellationToken).ConfigureAwait(false))
.Select(r => r.ToResult()).ToList();
var checkSettings = check.Settings;
var identity = new SyntheticCheckIdentity(check.CheckId, check.Name, check.Kind, checkSettings.Target(check.Kind))
{
ServerName = check.ServerName,
ServiceName = checkSettings.ServiceName,
WarnMilliseconds = checkSettings.WarnMilliseconds,
Https = checkSettings.Url?.Scheme == Uri.UriSchemeHttps,
};
var results = ScopedRuleEngine.Evaluate(SyntheticCheckRules.Create(), new SyntheticCheckRuleContext(identity, result.At, recent, settings));
var changes = await repository.SaveScopedRuleResultsAsync(FindingScope.Check, check.CheckId, results, result.At, cancellationToken).ConfigureAwait(false);
var events = ServerNotifications.For(check.Name, changes, notificationOptions.Value.WebUrl, result.At).ToList();
if (events.Count > 0)
{
await notifications.DispatchAsync(events, cancellationToken).ConfigureAwait(false);
}
if (IsNews(check.CheckId, result.Success ? "ok" : result.Message, remember: true))
{
LogResult(logger, check.Name, result.Success, result.Message);
}
return result;
}
/// <summary>Whether a check's state differs from the last one logged (the same failure every run is not news).</summary>
private bool IsNews(int checkId, string state, bool remember)
{
lock (_lock)
{
if (_reported.TryGetValue(checkId, out var previous) && previous == state)
{
return false;
}
if (remember)
{
_reported[checkId] = state;
}
return true;
}
}
[LoggerMessage(Level = LogLevel.Information, Message = "Synthetic check {Check}: success {Success}: {Message}")]
private static partial void LogResult(ILogger logger, string check, bool success, string message);
[LoggerMessage(Level = LogLevel.Error, Message = "Synthetic check {Check} could not be run")]
private static partial void LogCheckFailed(ILogger logger, Exception exception, string check);
[LoggerMessage(Level = LogLevel.Error, Message = "Running synthetic checks failed this round; retrying.")]
private static partial void LogRoundFailed(ILogger logger, Exception exception);
}

View file

@ -52,47 +52,6 @@
"EventDays": 90,
"ResolvedFindingDays": 365
},
"DynamicsAx": {
// Dynamics AX-tjek (docs/dynamics-ax.md). Kører kun i databaser genkendt som AX og valgt til overvågning.
"BatchLateMinutes": 30,
"BatchStuckHours": 6,
"BatchLongRunningFactor": 2.0,
"NumberSequenceWarnPercent": 80,
"ForecastWarnDays": 90,
"RecId32WarnPercent": 50
// "LocalTimeZone": "Romance Standard Time" // AOS'ens tidszone for Axapta 3.0/4.0's lokale tider; tom: agentens egen
},
"Servers": {
// Windows-servere (se docs/windows-servere.md). Rettigheder: db/permissions/grant-windows-server.ps1 – aldrig lokal administrator.
"Enabled": true,
"IncludeSqlHosts": true,
"Protocol": "Auto",
"MaxParallelServers": 4,
"OperationTimeout": "00:00:30",
"CycleTimeout": "00:05:00",
"Intervals": { "performance": "00:01:00", "disks": "00:15:00", "services": "00:05:00", "events": "00:05:00", "tasks": "00:15:00" },
"Cpu": { "HighPercent": 90, "SustainedMinutes": 15 },
"Memory": { "AvailablePercent": 5, "AvailableMegabytes": 1024, "PagesPerSecond": 1000, "CommitPercent": 90 },
"Disks": { "FreePercent": 10, "CriticalFreePercent": 5, "EnoughFreeGigabytes": 50, "ForecastDays": 30, "ForecastHistoryDays": 14 },
"Services": { "Ignore": [], "StartupGraceMinutes": 15, "MaxFindings": 15 },
"EventLog": { "RepeatThreshold": 5, "Ignore": [], "MaxFindings": 15 },
"Tasks": { "IncludeMicrosoftTasks": false, "IgnoreUserAndUpdaterTasks": true, "LateMinutes": 60, "MaxWatched": 50, "Ignore": [] },
"UptimeDays": 0,
"SampleDays": 14,
"DiskHistoryDays": 35,
"EventDays": 90
},
"SyntheticChecks": {
// Syntetiske tjek (se docs/syntetiske-tjek.md). Tjekkene selv oprettes i webbrugerfladen eller API'et.
"Enabled": true,
"MaxParallelChecks": 4,
"FailuresBeforeFinding": 2,
"SlowRunsBeforeFinding": 3,
"CertificateWarningDays": 21,
"CertificateCriticalDays": 7,
"ResultDays": 14,
"BusinessConnectorPath": null
},
"Instances": [
// Servere tilføjes nemmest i webbrugerfladen (Servere og databaser). Her kan de også stå, fx (se docs/installation.md):
// { "Name": "prod-sql01", "Server": "sql01.domæne.local", "Authentication": "Windows", "Provider": "Auto", "Encryption": "Mandatory" }

View file

@ -12,9 +12,6 @@ public static class BuiltInCollectors
public static IReadOnlyList<ICollector> Create(Core.CustomRules.CustomRuleCatalog? custom = null, CollectorSettings? settings = null)
{
settings ??= new CollectorSettings();
// One Dynamics AX detection per instance, shared by the AX collectors and the AX community rules.
var ax = new DynamicsAx.AxDatabaseDetector();
return
[
new ServerCollector(),
@ -43,10 +40,7 @@ public static class BuiltInCollectors
new IndexCollector(),
new AvailabilityGroupCollector(),
new AgentJobCollector(),
new DynamicsAx.DynamicsAxCollector(ax),
new DynamicsAx.AxBatchCollector(ax, settings.DynamicsAx),
new DynamicsAx.AxSequenceCollector(ax, settings.DynamicsAx),
.. custom is null ? Array.Empty<ICollector>() : [new Custom.CustomQueryCollector(custom, ax)],
.. custom is null ? Array.Empty<ICollector>() : [new Custom.CustomQueryCollector(custom)],
];
}
}
@ -61,7 +55,6 @@ public static class BuiltInCollectors
/// <param name="RequestMinutes">A statement running longer than this is a finding.</param>
/// <param name="VersionStoreMegabytes">The version store above this is a finding.</param>
/// <param name="TempdbSessionMegabytes">A session using more tempdb than this for 15 minutes is a finding.</param>
/// <param name="DynamicsAx">When the Dynamics AX checks raise findings (SQLInsight:DynamicsAx).</param>
public sealed record CollectorSettings(
Core.Data.IGroupMemberResolver? Groups = null,
int MaxSysadminAccounts = 3,
@ -71,5 +64,4 @@ public sealed record CollectorSettings(
int TransactionLogMegabytes = 1024,
int RequestMinutes = 15,
int VersionStoreMegabytes = 1024,
int TempdbSessionMegabytes = 1024,
Core.DynamicsAx.AxThresholds? DynamicsAx = null);
int TempdbSessionMegabytes = 1024);

View file

@ -29,9 +29,6 @@ public sealed class BlockingCollector : Collector<BlockingSnapshot>
: "(SELECT COUNT(*) FROM sys.dm_tran_session_transactions st WHERE st.session_id = s.session_id)";
var database = version.Major >= 11 ? "COALESCE(r.database_id, s.database_id)" : "r.database_id";
// CONTEXT_INFO, where Dynamics AX 2009/2012 AOS can put the AX user (sys.dm_exec_sessions has it from 2008).
var contextInfo = version.Major >= 10 ? "s.context_info" : "(SELECT TOP 1 p.context_info FROM sys.sysprocesses p WHERE p.spid = s.session_id AND p.ecid = 0)";
// blocking_session_id < 0 are special cases (orphaned DTC, deferred recovery, latch) and not a session.
return $"""
SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.status), r.wait_type, r.wait_time, r.wait_resource,
@ -41,7 +38,7 @@ public sealed class BlockingCollector : Collector<BlockingSnapshot>
(CASE WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END
- r.statement_start_offset) / 2 + 1)
ELSE t.text END, 4000),
s.last_request_end_time, {contextInfo}
s.last_request_end_time
FROM sys.dm_exec_sessions s
LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id
LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.session_id
@ -55,7 +52,7 @@ public sealed class BlockingCollector : Collector<BlockingSnapshot>
internal const string Sql2000 = """
SELECT p.spid, NULLIF(CASE WHEN p.blocked = p.spid THEN 0 ELSE p.blocked END, 0), RTRIM(p.status), RTRIM(p.lastwaittype),
p.waittime, RTRIM(p.waitresource), DB_NAME(p.dbid), RTRIM(p.loginame), RTRIM(p.hostname), RTRIM(p.program_name),
p.open_tran, CAST(NULL AS nvarchar(4000)), p.last_batch, p.context_info
p.open_tran, CAST(NULL AS nvarchar(4000)), p.last_batch
FROM master.dbo.sysprocesses p
WHERE (p.blocked > 0 AND p.blocked <> p.spid)
OR p.spid IN (SELECT blocked FROM master.dbo.sysprocesses WHERE blocked > 0 AND blocked <> spid)
@ -76,7 +73,6 @@ public sealed class BlockingCollector : Collector<BlockingSnapshot>
OpenTransactionCount = r.NullableInt(10) ?? 0,
SqlText = r.NullableString(11),
LastRequestEndTime = r.NullableDateTime(12),
ContextInfo = r.NullableHex(13),
};
private static SessionInfo Map2000(Row r) => Map2005(r);

View file

@ -51,17 +51,16 @@ public sealed class DeadlockCollector : Collector<DeadlockSnapshot>
? $"{Db.Literal(lastFile)}, {lastOffset.Value.ToString(CultureInfo.InvariantCulture)}"
: "NULL, NULL";
// One read of the files and no temp table: the deadlock rows carry their XML, and the newest row
// (rn = 1) is returned as well, only to know where reading stopped.
// Only the deadlock rows carry their XML; the rest are kept just to know where reading stopped.
return $"""
SELECT e.event_data, e.file_name, e.file_offset, e.rn
FROM (
SELECT CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data,
file_name, file_offset,
ROW_NUMBER() OVER (ORDER BY file_name DESC, file_offset DESC) AS rn
FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, {start})
) AS e
WHERE e.event_data IS NOT NULL OR e.rn = 1;
SELECT object_name, file_name, file_offset,
CASE WHEN object_name = N'xml_deadlock_report' THEN CAST(event_data AS nvarchar(max)) END AS event_data
INTO #system_health
FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, {start});
SELECT event_data FROM #system_health WHERE event_data IS NOT NULL;
SELECT TOP (1) file_name, file_offset FROM #system_health ORDER BY file_name DESC, file_offset DESC;
DROP TABLE #system_health;
""";
}
@ -85,17 +84,14 @@ public sealed class DeadlockCollector : Collector<DeadlockSnapshot>
await using (var reader = await command.ExecuteReaderAsync(ct).ConfigureAwait(false))
{
while (await reader.ReadAsync(ct).ConfigureAwait(false))
{
if (!reader.IsDBNull(0))
{
events.Add(reader.GetString(0));
}
if (Convert.ToInt64(reader.GetValue(3), CultureInfo.InvariantCulture) == 1)
if (await reader.NextResultAsync(ct).ConfigureAwait(false) && await reader.ReadAsync(ct).ConfigureAwait(false))
{
file = reader.GetString(1);
offset = Convert.ToInt64(reader.GetValue(2), CultureInfo.InvariantCulture);
}
file = reader.GetString(0);
offset = Convert.ToInt64(reader.GetValue(1), CultureInfo.InvariantCulture);
}
}

View file

@ -6,10 +6,9 @@ namespace SQLInsight.Collectors.Custom;
/// <summary>
/// Runs the queries of the custom rules (community and local). Each rule gets its own result or error, so one broken
/// or unpermitted query only affects its own rule. Dynamics AX rules (dynamicsAx) only run in the monitored databases the
/// shared AX detection recognises; elsewhere they send no query at all.
/// or unpermitted query only affects its own rule.
/// </summary>
public sealed class CustomQueryCollector(CustomRuleCatalog catalog, DynamicsAx.AxDatabaseDetector? ax = null) : Collector<CustomQueryResults>
public sealed class CustomQueryCollector(CustomRuleCatalog catalog) : Collector<CustomQueryResults>
{
/// <summary>SQL Server error numbers that mean the login lacks a permission or access to a database.</summary>
private static readonly HashSet<int> PermissionErrors = [229, 230, 262, 297, 300, 916, 4060, 15247, 15151];
@ -24,8 +23,6 @@ public sealed class CustomQueryCollector(CustomRuleCatalog catalog, DynamicsAx.A
public override string Name => "custom-rules";
public override TimeSpan DefaultInterval => TimeSpan.FromMinutes(15);
private readonly DynamicsAx.AxDatabaseDetector _ax = ax ?? new DynamicsAx.AxDatabaseDetector();
protected override async Task<CustomQueryResults> CollectAsync(CollectorContext context, CustomQueryResults? previous)
{
ArgumentNullException.ThrowIfNull(context);
@ -40,7 +37,6 @@ public sealed class CustomQueryCollector(CustomRuleCatalog catalog, DynamicsAx.A
try
{
List<string>? databases = null;
HashSet<string>? axDatabases = null;
foreach (var rule in rules)
{
if (rule.Scope == CustomRuleScope.Server)
@ -51,19 +47,6 @@ public sealed class CustomQueryCollector(CustomRuleCatalog catalog, DynamicsAx.A
databases ??= (await Db.QueryAsync(context.Connection, DatabasesSql, r => r.String(0), context.CancellationToken).ConfigureAwait(false))
.Where(context.IncludesDatabase).ToList();
if (rule.DynamicsAx)
{
// The AX detection needs SQL Server 2005 (sys.columns); AX rules have nowhere to run before that.
axDatabases ??= context.Instance.Version.Major >= 9
? (await _ax.GetAsync(context).ConfigureAwait(false)).Databases.Select(d => d.Database).ToHashSet(StringComparer.OrdinalIgnoreCase)
: [];
var targets = databases.Where(axDatabases.Contains).ToList();
results[rule.Id] = targets.Count == 0
? new CustomQueryResult([], NotRelevant: "Ikke relevant: ingen af de overvågede databaser på instansen er genkendt som Dynamics AX.")
: await RunPerDatabaseAsync(context, rule, targets).ConfigureAwait(false);
continue;
}
results[rule.Id] = await RunPerDatabaseAsync(context, rule, databases).ConfigureAwait(false);
}
}

View file

@ -11,14 +11,11 @@ internal static class Db
{
public const int CommandTimeoutSeconds = 30;
public static Task<List<T>> QueryAsync<T>(DbConnection connection, string sql, Func<Row, T> map, CancellationToken cancellationToken) =>
QueryAsync(connection, sql, map, CommandTimeoutSeconds, cancellationToken);
public static async Task<List<T>> QueryAsync<T>(DbConnection connection, string sql, Func<Row, T> map, int timeoutSeconds, CancellationToken cancellationToken)
public static async Task<List<T>> QueryAsync<T>(DbConnection connection, string sql, Func<Row, T> map, CancellationToken cancellationToken)
{
await using var command = connection.CreateCommand();
command.CommandText = sql;
command.CommandTimeout = timeoutSeconds;
command.CommandTimeout = CommandTimeoutSeconds;
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
var rows = new List<T>();
var row = new Row(reader);
@ -40,32 +37,6 @@ internal static class Db
/// <summary>An N'…' string literal with quotes escaped.</summary>
public static string Literal(string value) => "N'" + value.Replace("'", "''", StringComparison.Ordinal) + "'";
/// <summary>
/// SQL Server's error number: SqlClient exposes it as Number, ODBC per error as NativeError. Null when unknown
/// (also for a command timeout, which SqlClient reports as -2).
/// </summary>
public static int? ErrorNumber(DbException exception)
{
ArgumentNullException.ThrowIfNull(exception);
if (exception.GetType().GetProperty("Number")?.GetValue(exception) is int number)
{
return number;
}
if (exception.GetType().GetProperty("Errors")?.GetValue(exception) is System.Collections.IEnumerable errors)
{
foreach (var error in errors)
{
if (error.GetType().GetProperty("NativeError")?.GetValue(error) is int native && native != 0)
{
return native;
}
}
}
return null;
}
/// <summary>A [bracketed] identifier with brackets escaped.</summary>
public static string Identifier(string name) => "[" + name.Replace("]", "]]", StringComparison.Ordinal) + "]";
@ -134,21 +105,4 @@ internal sealed class Row(DbDataReader reader)
public DateTime? NullableDateTime(int ordinal) => reader.IsDBNull(ordinal) ? null : DateTime(ordinal);
public int FieldCount => reader.FieldCount;
/// <summary>A binary value as hex without trailing zero bytes (CONTEXT_INFO is padded), or null when null or all zeros.</summary>
public string? NullableHex(int ordinal)
{
if (reader.IsDBNull(ordinal) || reader.GetValue(ordinal) is not byte[] bytes)
{
return null;
}
var length = bytes.Length;
while (length > 0 && bytes[length - 1] == 0)
{
length--;
}
return length == 0 ? null : Convert.ToHexString(bytes, 0, length);
}
}

View file

@ -1,6 +1,5 @@
using System.Data.Common;
using SQLInsight.Core.Configuration;
using SQLInsight.Core.DynamicsAx;
using SQLInsight.Core.Instances;
namespace SQLInsight.Collectors.Discovery;
@ -8,7 +7,7 @@ namespace SQLInsight.Collectors.Discovery;
/// <summary>
/// Lists every database the agent can see on an instance, with state, size, recovery model and compatibility level,
/// and recognises Dynamics AX databases. Only metadata is read (sys.databases, sys.master_files, sys.tables and
/// sys.columns, or their SQL Server 2000 counterparts), through three-part names and [db].sys.sp_executesql: the session never changes database
/// sys.columns, or their SQL Server 2000 counterparts), through three-part names: the session never changes database
/// and nothing is written. Works from SQL Server 2000 (over the legacy ODBC connection) and up.
/// </summary>
public static class DatabaseDiscovery
@ -38,16 +37,33 @@ public static class DatabaseDiscovery
""";
/// <summary>
/// The central Dynamics AX detection (Core/DynamicsAx/AxDetection.cs), the same as the AX collectors and the AX
/// community rules use: run in the database through [db].sys.sp_executesql on SQL Server 2005 and later, with three-part
/// names to sysobjects on SQL Server 2000. The session never changes database.
/// The Dynamics AX signature, the same as the AX community rules use (rules/community/dynamics-ax.json): the system
/// tables SQLDICTIONARY, DATAAREA and SYSCONFIG, in any schema (AX 3.0 owns them as bmssa). RECID on SQLDICTIONARY
/// is int (precision 10) in AX 3.0 and bigint (19) from AX 4.0.
/// </summary>
public static string DynamicsAxSql(string database, SqlServerVersion version)
{
ArgumentNullException.ThrowIfNull(database);
return version.Major >= 9 ? AxSql.InDatabase(database, AxDetection.MetadataSql) : AxDetection.MetadataSql2000(database);
var db = Db.Identifier(database);
return version.Major >= 9
? $"""
SELECT COUNT(DISTINCT t.name), MAX(CASE WHEN t.name = N'SQLDICTIONARY' THEN c.precision END)
FROM {db}.sys.tables t
LEFT JOIN {db}.sys.columns c ON c.object_id = t.object_id AND c.name = N'RECID'
WHERE t.name IN (N'SQLDICTIONARY', N'DATAAREA', N'SYSCONFIG')
"""
: $"""
SELECT COUNT(DISTINCT o.name), MAX(CASE WHEN o.name = N'SQLDICTIONARY' THEN c.xprec END)
FROM {db}.dbo.sysobjects o
LEFT JOIN {db}.dbo.syscolumns c ON c.id = o.id AND c.name = N'RECID'
WHERE o.xtype = 'U' AND o.name IN (N'SQLDICTIONARY', N'DATAAREA', N'SYSCONFIG')
""";
}
/// <summary>"3.0" or "4.0+" when all three AX system tables are there, else null.</summary>
public static string? DynamicsAxVersion(int tables, int? recIdPrecision) =>
tables < 3 ? null : recIdPrecision is 10 ? "3.0" : "4.0+";
public static async Task<List<DiscoveredDatabase>> DiscoverAsync(DbConnection connection, InstanceInfo instance, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(connection);
@ -66,7 +82,6 @@ public static class DatabaseDiscovery
IsSystem = DatabaseSelection.IsSystemDatabase(r.String(0)),
}, cancellationToken).ConfigureAwait(false);
var names = databases.Select(d => d.Name).ToList();
for (var i = 0; i < databases.Count; i++)
{
var database = databases[i];
@ -77,12 +92,11 @@ public static class DatabaseDiscovery
try
{
var rows = await Db.QueryAsync(connection, DynamicsAxSql(database.Name, instance.Version),
r => new AxDetectionRow(r.String(0), r.String(1), r.String(2), r.NullableString(3) ?? "?", r.NullableInt(4) ?? 0, r.NullableInt(5) ?? 0, r.Bool(6), r.NullableString(7)),
AxSql.CommandTimeoutSeconds, cancellationToken).ConfigureAwait(false);
if (AxDetection.Recognise(database.Name, rows, names) is { } ax)
var rows = await Db.QueryAsync(connection, DynamicsAxSql(database.Name, instance.Version), r => (Tables: r.Int(0), Precision: r.NullableInt(1)), cancellationToken)
.ConfigureAwait(false);
if (rows.Count > 0 && DynamicsAxVersion(rows[0].Tables, rows[0].Precision) is { } version)
{
databases[i] = database with { IsDynamicsAx = true, DynamicsAxVersion = AxVersions.ShortText(ax.Version) };
databases[i] = database with { IsDynamicsAx = true, DynamicsAxVersion = version };
}
}
catch (DbException)

View file

@ -1,167 +0,0 @@
using System.Globalization;
using SQLInsight.Core.DynamicsAx;
using SQLInsight.Core.Instances;
namespace SQLInsight.Collectors.DynamicsAx;
/// <summary>
/// Batch jobs in every monitored AX database where the agent may read the batch tables (optional AX read access),
/// every 5 minutes: status, planned and actual start, and completed runs (from BATCHJOBHISTORY in AX 2009 and later,
/// from what SQLInsight itself sees in AX 3.0/4.0). Each job is compared with its own history, kept by SQLInsight in
/// its repository (insight.AxBatchRuns). Only standard AX tables are read; bounded with TOP; never written to.
/// </summary>
public sealed class AxBatchCollector(AxDatabaseDetector detector, AxThresholds? thresholds = null) : Collector<AxBatchSnapshot>
{
/// <summary>Run keys remembered to avoid storing a run twice; more than this and the set starts over (the repository also ignores duplicates).</summary>
private const int RecentRunKeysKept = 20_000;
/// <summary>Finished rows older than this are not kept in the snapshot (AX 3.0 keeps one BATCH row per recurrence).</summary>
private static readonly TimeSpan FinishedShown = TimeSpan.FromDays(7);
private const int JobsShown = 500;
/// <summary>
/// A lock timeout keeps the earlier picture (so findings do not flap), but no longer than this: running and late times
/// keep growing against an old picture, and a job that has started meanwhile would still be reported late.
/// </summary>
internal static readonly TimeSpan KeptThroughLockTimeouts = TimeSpan.FromMinutes(15);
private readonly AxThresholds _thresholds = thresholds ?? new AxThresholds();
public override string Name => "dynamics-ax-batch";
public override TimeSpan DefaultInterval => TimeSpan.FromMinutes(5);
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.DynamicManagementViews];
protected override async Task<AxBatchSnapshot> CollectAsync(CollectorContext context, AxBatchSnapshot? previous)
{
var ax = await detector.GetAsync(context).ConfigureAwait(false);
var history = previous?.History.Clone() ?? new AxBatchHistory();
var firstSeen = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
var recentKeys = previous?.RecentRunKeys is { Count: < RecentRunKeysKept } kept ? new HashSet<string>(kept) : [];
var databases = new List<AxBatchDatabase>();
foreach (var database in ax.Databases)
{
databases.Add(await CollectDatabaseAsync(context, database, previous, history, firstSeen, recentKeys, _thresholds.LocalTimeZone).ConfigureAwait(false));
}
return new AxBatchSnapshot
{
CapturedAt = context.Now,
Thresholds = _thresholds,
Databases = databases,
History = history,
FirstSeenExecuting = firstSeen,
RecentRunKeys = recentKeys,
};
}
private static async Task<AxBatchDatabase> CollectDatabaseAsync(CollectorContext context, AxDatabase database, AxBatchSnapshot? previous, AxBatchHistory history,
Dictionary<string, DateTime> firstSeen, HashSet<string> recentKeys, string? localTimeZone)
{
var name = database.Database;
var earlier = previous?.Databases.FirstOrDefault(d => string.Equals(d.Database, name, StringComparison.OrdinalIgnoreCase));
if (AxSchemaAdapter.For(database.Version) is not { } adapter)
{
return new AxBatchDatabase { Database = name, Version = database.Version, NotMeasurable = "Ikke målbar: AX-versionen kunne ikke bestemmes ud fra skemaet." };
}
var query = adapter.BatchJobs(database);
var (rows, problem) = await AxReader.ReadAsync(context, database, query, Map).ConfigureAwait(false);
if (rows is null)
{
// A lock timeout keeps the previous picture for a while (marked as such), so findings do not flap.
return AxReader.IsTransient(problem) && earlier is { NotMeasurable: null }
&& context.Now - (earlier.MeasuredAt ?? previous!.CapturedAt) <= KeptThroughLockTimeouts
? earlier with { NewRuns = [], Notes = [.. earlier.Notes.Where(n => n != AxReader.LockTimeoutText), AxReader.LockTimeoutText] }
: new AxBatchDatabase { Database = name, Version = database.Version, NotMeasurable = problem };
}
var notes = new List<string>(query.Notes);
// Axapta 3.0/4.0 store local times: they become UTC in the AX time zone (see AxLocalTime).
var (zone, zoneNote) = adapter.LocalBatchTimes && rows.Count > 0
? AxLocalTime.Resolve(localTimeZone, rows[0].ServerUtcOffsetMinutes, context.Now.UtcDateTime)
: (null, null);
if (zoneNote is not null)
{
notes.Add(zoneNote);
}
var newRuns = new List<AxBatchRun>();
long? watermark = earlier?.HistoryWatermark;
// AX 2009 and later: completed runs from AX's own history, by RECID.
var historyQuery = adapter.BatchHistory(database, watermark);
var (historyRows, historyProblem) = await AxReader.ReadAsync(context, database, historyQuery, MapHistory).ConfigureAwait(false);
var fromHistoryTable = historyRows is not null;
if (historyRows is not null)
{
foreach (var row in historyRows)
{
watermark = Math.Max(watermark ?? 0, row.RecId);
if (adapter.HistoryRun(row) is { } run && recentKeys.Add(AxBatchHistory.ScopedRunKey(name, run.RunKey)))
{
newRuns.Add(run);
}
}
}
else if (historyProblem is not null)
{
notes.Add("Kørselshistorik fra BATCHJOBHISTORY: " + historyProblem + " SQLInsight bruger i stedet de kørsler, den selv ser.");
}
// Pass 1: when jobs were first seen executing, and (AX 3.0/4.0, or no history access) runs SQLInsight saw finish.
var prelim = new List<(AxBatchJobRow Row, AxBatchJob Job)>();
foreach (var row in rows)
{
var seenKey = AxBatchHistory.Key(name, "#" + row.RecId.ToString(CultureInfo.InvariantCulture));
DateTime? seenBefore = previous?.FirstSeenExecuting.TryGetValue(seenKey, out var seen) == true ? seen : null;
var job = adapter.Interpret(row, history, name, null, zone);
if (job.Status == AxBatchStatus.Executing)
{
firstSeen[seenKey] = seenBefore ?? context.Now.UtcDateTime;
}
else if (!fromHistoryTable && AxSchemaAdapter.ObservedRun(job, seenBefore) is { } run && recentKeys.Add(AxBatchHistory.ScopedRunKey(name, run.RunKey)))
{
newRuns.Add(run);
}
prelim.Add((row, job));
}
foreach (var run in newRuns)
{
history.Add(name, run);
}
// Pass 2: the jobs with their history's statistics, the interesting ones first.
var now = context.Now.UtcDateTime;
var jobs = prelim
.Select(p => adapter.Interpret(p.Row, history, name,
firstSeen.TryGetValue(AxBatchHistory.Key(name, "#" + p.Row.RecId.ToString(CultureInfo.InvariantCulture)), out var s) ? s : null, zone))
.Where(j => j.Status is not (AxBatchStatus.Finished or AxBatchStatus.Canceled) || (j.EndedUtc ?? j.PlannedStartUtc ?? now) >= now - FinishedShown)
.OrderBy(j => j.Status switch { AxBatchStatus.Executing => 0, AxBatchStatus.Error => 1, AxBatchStatus.Waiting => 2, _ => 3 })
.ThenByDescending(j => j.RecId)
.Take(JobsShown)
.ToList();
return new AxBatchDatabase
{
Database = name,
Version = database.Version,
Jobs = jobs,
NewRuns = newRuns,
StatusCounts = rows.GroupBy(r => r.Status).ToDictionary(g => g.Key, g => g.Count()),
Notes = notes,
HistoryWatermark = watermark,
MeasuredAt = context.Now,
};
}
private static AxBatchJobRow Map(Row r) => new(
r.Long(0), r.NullableInt(1), r.NullableString(2), r.NullableString(3), r.NullableString(4), r.Int(5),
r.NullableDateTime(6), r.NullableInt(7), r.NullableDateTime(8), r.NullableInt(9), r.NullableDateTime(10), r.NullableInt(11), r.Int(12));
private static AxBatchHistoryRow MapHistory(Row r) => new(
r.Long(0), r.NullableLong(1), r.NullableString(2), r.NullableString(3), r.Int(4), r.NullableDateTime(5), r.NullableDateTime(6), r.Int(7));
}

View file

@ -1,160 +0,0 @@
using System.Data.Common;
using SQLInsight.Core.DynamicsAx;
using SQLInsight.Core.Instances;
namespace SQLInsight.Collectors.DynamicsAx;
/// <summary>
/// Finds the Dynamics AX databases on one instance with the central detection (<see cref="AxDetection"/>) and keeps
/// the answer for <see cref="Refresh"/>, so new and removed AX databases are noticed within the hour. Shared by
/// everything AX-specific on the instance (the AX collectors and the AX community rules). Only databases chosen for
/// monitoring are looked at; reads metadata, plus the company list when the agent may read DATAAREA. Not thread-safe:
/// one instance's collectors run one at a time.
/// </summary>
public sealed class AxDatabaseDetector
{
public static readonly TimeSpan Refresh = TimeSpan.FromHours(1);
/// <summary>An AX database without the full read access is looked at again sooner, so a GRANT takes effect within minutes.</summary>
public static readonly TimeSpan RefreshWithoutReadAccess = TimeSpan.FromMinutes(5);
private static TimeSpan RefreshFor(AxDatabase? database) => database is not null && AxReadAccess.Missing(database).Count > 0 ? RefreshWithoutReadAccess : Refresh;
private readonly Dictionary<string, (DateTimeOffset At, AxDatabase? Database, string? Problem)> _cache = new(StringComparer.OrdinalIgnoreCase);
private string? _login;
/// <summary>Online user databases the login can enter, with their snapshot settings.</summary>
internal const string DatabasesSql = """
SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on
FROM sys.databases
WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1
ORDER BY name
""";
/// <summary>The agent's login, for the GRANT text shown when AX read access is missing.</summary>
internal const string LoginSql = "SELECT SUSER_SNAME()";
/// <summary>The detection, run in each monitored database through [db].sys.sp_executesql (metadata only).</summary>
internal static string BuildDetectionSql(string database) => AxSql.InDatabase(database, AxDetection.MetadataSql);
/// <summary>The monitored AX databases, from the cache when younger than <see cref="Refresh"/> (unless <paramref name="force"/>).</summary>
public async Task<AxDatabaseSnapshot> GetAsync(CollectorContext context, bool force = false)
{
ArgumentNullException.ThrowIfNull(context);
var connection = context.Connection;
var ct = context.CancellationToken;
var databases = await Db.QueryAsync(connection, DatabasesSql, r => (Name: r.String(0), Snapshot: r.Int(1), Rcsi: r.Bool(2)), ct).ConfigureAwait(false);
_login ??= (await Db.QueryAsync(connection, LoginSql, r => r.NullableString(0), ct).ConfigureAwait(false)).FirstOrDefault();
var names = databases.Select(d => d.Name).ToList();
var found = new List<AxDatabase>();
var problems = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var (name, snapshot, rcsi) in databases)
{
if (!context.IncludesDatabase(name) || name.EndsWith("_model", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (force || !_cache.TryGetValue(name, out var cached) || context.Now - cached.At >= RefreshFor(cached.Database))
{
cached = await DetectAsync(context, name, names, _cache.TryGetValue(name, out var earlier) ? earlier.Database : null).ConfigureAwait(false);
_cache[name] = cached;
}
if (cached.Problem is { } problem)
{
problems[name] = problem;
}
if (cached.Database is { } ax)
{
found.Add(ax with
{
AgentLogin = _login,
Isolation = rcsi ? AxReadIsolation.ReadCommittedSnapshot : snapshot == 1 ? AxReadIsolation.Snapshot : AxReadIsolation.ReadCommitted,
});
}
}
foreach (var gone in _cache.Keys.Where(k => !names.Contains(k, StringComparer.OrdinalIgnoreCase)).ToList())
{
_cache.Remove(gone);
}
return new AxDatabaseSnapshot { CapturedAt = context.Now, Databases = found, AgentLogin = _login, Problems = problems };
}
/// <param name="earlier">What the previous detection found: kept through a passing error (a lock timeout during an AX
/// table synchronisation, a deadlock), so the database does not drop out of AX monitoring for an hour.</param>
private async Task<(DateTimeOffset, AxDatabase?, string?)> DetectAsync(CollectorContext context, string database, IReadOnlyList<string> names, AxDatabase? earlier)
{
try
{
var rows = await Db.QueryAsync(context.Connection, BuildDetectionSql(database),
r => new AxDetectionRow(r.String(0), r.String(1), r.String(2), r.NullableString(3) ?? "?", r.NullableInt(4) ?? 0, r.NullableInt(5) ?? 0, r.Bool(6), r.NullableString(7)),
AxSql.CommandTimeoutSeconds, context.CancellationToken).ConfigureAwait(false);
if (AxDetection.Recognise(database, rows, names) is not { } ax)
{
return (context.Now, null, null);
}
ax = ax with { AgentLogin = _login };
if (AxSchemaAdapter.For(ax.Version) is not null)
{
var (companies, reason) = await AxReader.ReadAsync(context, ax, AxSchemaAdapter.Companies(ax),
r => new AxCompany(r.String(0), r.NullableString(1), r.Bool(2))).ConfigureAwait(false);
ax = ax with { Companies = companies, CompaniesReason = reason };
}
return (context.Now, ax, null);
}
catch (DbException ex) when (!context.CancellationToken.IsCancellationRequested)
{
if (earlier is not null && Db.ErrorNumber(ex) is AxSql.LockTimeoutError or 1205 or null)
{
// A passing error: keep what was found and try again within minutes.
return (context.Now - Refresh + RefreshWithoutReadAccess, earlier, null);
}
// No access to its metadata (e.g. no database user before 2014): it cannot be told apart, and nothing AX-specific runs there.
return (context.Now, null, "AX-genkendelsen kunne ikke læse databasens metadata: " + ex.Message);
}
}
}
/// <summary>Reads an AX query: the rows, or why not (Danish). A lock timeout or a missing permission is a reason, never an exception.</summary>
internal static class AxReader
{
public const string LockTimeoutText = "Ikke målt denne gang: AX-tabellen var låst i mere end 2 sekunder, og SQLInsight venter ikke på AX' låse. Prøves igen ved næste indsamling.";
public static async Task<(List<T>? Rows, string? Problem)> ReadAsync<T>(CollectorContext context, AxDatabase database, AxQuery? query, Func<Row, T> map)
{
if (query is null)
{
return (null, null);
}
if (query.Sql is not { } sql)
{
return (null, query.NotMeasurable);
}
try
{
return (await Db.QueryAsync(context.Connection, AxSql.InDatabase(database.Database, sql, database.Isolation), map, AxSql.CommandTimeoutSeconds,
context.CancellationToken).ConfigureAwait(false), null);
}
catch (DbException ex) when (!context.CancellationToken.IsCancellationRequested)
{
// (A cancelled collection is not an AX problem: SqlClient reports it as a DbException, which is passed on.)
return (null, Db.ErrorNumber(ex) switch
{
AxSql.LockTimeoutError => LockTimeoutText,
229 or 230 or 297 or 916 => "Ikke målbar: adgang nægtet. " + AxReadAccess.NotMeasurable(database, AxReadAccess.Tables(database)),
_ => "Ikke målbar: læsningen fejlede (" + ex.Message + ").",
});
}
}
public static bool IsTransient(string? problem) => problem == LockTimeoutText;
}

View file

@ -1,105 +0,0 @@
using SQLInsight.Core.DynamicsAx;
using SQLInsight.Core.Instances;
namespace SQLInsight.Collectors.DynamicsAx;
/// <summary>
/// Number sequences (NUMBERSEQUENCETABLE) and RecId allocation (SYSTEMSEQUENCES) in every monitored AX database where
/// the agent may read them, every hour. SQLInsight samples NEXTREC and NEXTVAL once a day into its own repository
/// (insight.AxSequenceSamples, bounded retention) and forecasts from that history when each runs out. Bounded with TOP
/// (the most used first); never written to.
/// </summary>
public sealed class AxSequenceCollector(AxDatabaseDetector detector, AxThresholds? thresholds = null) : Collector<AxSequenceSnapshot>
{
private readonly AxThresholds _thresholds = thresholds ?? new AxThresholds();
public override string Name => "dynamics-ax-sequences";
/// <summary>How many days of daily samples the forecasts use (and the agent reads back after a restart).</summary>
public int HistoryDays => _thresholds.ForecastBasisDays + 1;
public override TimeSpan DefaultInterval => TimeSpan.FromHours(1);
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.DynamicManagementViews];
protected override async Task<AxSequenceSnapshot> CollectAsync(CollectorContext context, AxSequenceSnapshot? previous)
{
var ax = await detector.GetAsync(context).ConfigureAwait(false);
var now = context.Now.UtcDateTime;
var history = new Dictionary<string, IReadOnlyList<AxSample>>(StringComparer.Ordinal);
var databases = new List<AxSequenceDatabase>();
foreach (var database in ax.Databases)
{
var name = database.Database;
var earlier = previous?.Databases.FirstOrDefault(d => string.Equals(d.Database, name, StringComparison.OrdinalIgnoreCase));
if (AxSchemaAdapter.For(database.Version) is not { } adapter)
{
const string unknown = "Ikke målbar: AX-versionen kunne ikke bestemmes ud fra skemaet.";
databases.Add(new AxSequenceDatabase { Database = name, Version = database.Version, NumberSequencesNotMeasurable = unknown, RecIdsNotMeasurable = unknown });
continue;
}
IReadOnlyList<AxSample> Samples(char kind, string key, long value)
{
var historyKey = AxSequenceSnapshot.HistoryKey(name, kind, key);
var samples = (previous?.History.TryGetValue(historyKey, out var stored) == true ? stored : [])
.Where(s => s.SampledUtc >= now.AddDays(-HistoryDays) && s.SampledUtc.Date != now.Date)
.Append(new AxSample(now, value))
.ToList();
history[historyKey] = samples;
return samples;
}
var notes = new List<string>();
var numbersQuery = adapter.NumberSequences(database);
notes.AddRange(numbersQuery.Notes);
var (numberRows, numberProblem) = await AxReader.ReadAsync(context, database, numbersQuery, MapNumberSequence).ConfigureAwait(false);
// Axapta 3.0/4.0 store the last cleanup as local time: it becomes UTC in the AX time zone (see AxLocalTime).
var (zone, zoneNote) = adapter.LocalBatchTimes && numberRows is { Count: > 0 }
? AxLocalTime.Resolve(_thresholds.LocalTimeZone, numberRows[0].ServerUtcOffsetMinutes, now)
: (null, null);
if (zoneNote is not null)
{
notes.Add(zoneNote);
}
var sequences = numberRows?.Select(r => adapter.Interpret(r, Samples(AxSequenceSnapshot.NumberSequenceKind, adapter.SequenceKey(r), r.Next), now,
_thresholds.ForecastBasisDays, zone)).ToList();
var recIdQuery = adapter.RecIds(database);
notes.AddRange(recIdQuery.Notes);
var nextValIsInt = string.Equals(database.Table("SYSTEMSEQUENCES")?.Column("NEXTVAL")?.Type, "int", StringComparison.OrdinalIgnoreCase);
var (recIdRows, recIdProblem) = await AxReader.ReadAsync(context, database, recIdQuery, MapRecId).ConfigureAwait(false);
var recIds = recIdRows?.Select(r => AxSchemaAdapter.Interpret(r, nextValIsInt, Samples(AxSequenceSnapshot.RecIdKind, AxSchemaAdapter.RecIdKey(r), r.NextVal), now,
_thresholds.ForecastBasisDays)).ToList();
// A lock timeout keeps the previous values, so findings do not flap.
databases.Add(new AxSequenceDatabase
{
Database = name,
Version = database.Version,
NumberSequences = sequences ?? (AxReader.IsTransient(numberProblem) ? earlier?.NumberSequences ?? [] : []),
NumberSequencesNotMeasurable = sequences is null && !(AxReader.IsTransient(numberProblem) && earlier?.NumberSequencesNotMeasurable is null && earlier is not null) ? numberProblem : null,
RecIds = recIds ?? (AxReader.IsTransient(recIdProblem) ? earlier?.RecIds ?? [] : []),
RecIdsNotMeasurable = recIds is null && !(AxReader.IsTransient(recIdProblem) && earlier?.RecIdsNotMeasurable is null && earlier is not null) ? recIdProblem : null,
Notes = notes,
});
// Sequences not read this time keep their history (a lock timeout must not lose it).
foreach (var (key, samples) in previous?.History ?? new Dictionary<string, IReadOnlyList<AxSample>>())
{
if (key.StartsWith(name + "\u001f", StringComparison.Ordinal) && !history.ContainsKey(key))
{
history[key] = samples.Where(s => s.SampledUtc >= now.AddDays(-HistoryDays)).ToList();
}
}
}
return new AxSequenceSnapshot { CapturedAt = context.Now, Thresholds = _thresholds, Databases = databases, History = history };
}
private static AxNumberSequenceRow MapNumberSequence(Row r) => new(
r.Long(0), r.String(1), r.NullableString(2), r.NullableString(3), r.Long(4), r.Long(5), r.Long(6), r.NullableString(7),
r.Bool(8), r.Bool(9), r.Bool(10), r.Bool(11), r.IsNull(12) ? null : r.Bool(12), r.NullableDateTime(13), r.NullableInt(14),
r.IsNull(15) ? null : r.Double(15), r.Int(16));
private static AxRecIdRow MapRecId(Row r) => new(r.Int(0), r.NullableString(1), r.Long(2), r.NullableLong(3), r.NullableString(4));
}

View file

@ -1,19 +0,0 @@
using SQLInsight.Core.DynamicsAx;
using SQLInsight.Core.Instances;
namespace SQLInsight.Collectors.DynamicsAx;
/// <summary>
/// Which monitored databases on the instance are Dynamics AX, with version, schema, companies and the agent's read
/// access, every hour (collector "dynamics-ax"). The AX pages in SQLInsight Web are built on it.
/// </summary>
public sealed class DynamicsAxCollector(AxDatabaseDetector detector) : Collector<AxDatabaseSnapshot>
{
public override string Name => "dynamics-ax";
public override TimeSpan DefaultInterval => AxDatabaseDetector.Refresh;
/// <summary>The detection uses sys.tables, sys.columns and HAS_PERMS_BY_NAME: SQL Server 2005 and later.</summary>
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.DynamicManagementViews];
protected override Task<AxDatabaseSnapshot> CollectAsync(CollectorContext context, AxDatabaseSnapshot? previous) => detector.GetAsync(context, force: true);
}

View file

@ -25,8 +25,6 @@ public sealed class DocsService
("analyse", "Hvad skete der i et bestemt tidsrum: belastning, ventetider mod normalen, tungeste forespørgsler, blocking og deadlocks."),
("regler", "Skriv egne regler i SQL, og bidrag med community-regler."),
("servere-og-databaser", "Tilmeld servere i webbrugerfladen, vælg databaser, test forbindelsen og få GRANT-scriptet."),
("windows-servere", "CPU, hukommelse, diske med prognose, tjenester, hændelseslog og planlagte opgaver på Windows-servere – uden lokal administrator."),
("syntetiske-tjek", "TCP, HTTP(S), SQL-login, DNS og Dynamics AX-logon som en bruger: opdag en server, der ikke svarer, selv om tjenesten kører."),
("support", "Opret supportsager hos SEOSoft fra webbrugerfladen og følg deres status."),
("databehandling", "Hvilke data der behandles, hvor de ligger, og hvad der deles."),
("legacy-versions", "SQL Server 2000, 2005 og 2008: hvad der kan måles, og hvordan der forbindes."),

View file

@ -28,15 +28,12 @@ public static class RuleCatalog
["BLK"] = "Blocking og deadlocks",
["DLK"] = "Blocking og deadlocks",
["SEC"] = "Sikkerhed",
["WIN"] = "Windows-servere",
["SYN"] = "Syntetiske tjek",
};
public static IReadOnlyList<RuleEntry> All(IReadOnlyList<CustomRuleDefinition> community)
{
ArgumentNullException.ThrowIfNull(community);
IEnumerable<RuleEntry> builtIn = BuiltInRules.Create().Select(r => new RuleEntry(r.Id, r.Title, Area(r.Id), null, false))
.Concat(Rules.Servers.ServerRuleCatalog.All.Select(r => new RuleEntry(r.Id, r.Title, Area(r.Id), null, false)));
IEnumerable<RuleEntry> builtIn = BuiltInRules.Create().Select(r => new RuleEntry(r.Id, r.Title, Area(r.Id), null, false));
return builtIn.Concat(community.Select(r => new RuleEntry(r.Id, r.Title, Area(r.Id), r.Author, true))).ToList();
}

View file

@ -12,7 +12,7 @@ public sealed class SqlConnectionFactory : ISqlConnectionFactory
// Keeps the monitoring session from ever being the one that blocks or wins a deadlock on the monitored server.
// Every statement here is valid on SQL Server 2000.
internal const string SessionSettings =
Core.Instances.AgentSession.Settings;
"SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;";
private readonly Func<ConnectionTarget, DbConnection> _createSqlClient;
private readonly Func<ConnectionTarget, DbConnection> _createOdbc;

View file

@ -116,10 +116,10 @@ public sealed record DiscoveredDatabase
/// <summary>The agent can enter the database (HAS_DBACCESS); without it, database-specific checks cannot run there.</summary>
public bool HasAccess { get; init; }
/// <summary>Recognised as Dynamics AX by the central detection (Core/DynamicsAx/AxDetection.cs).</summary>
/// <summary>The Dynamics AX system tables (SQLDICTIONARY, DATAAREA, SYSCONFIG) are there.</summary>
public bool IsDynamicsAx { get; init; }
/// <summary>The AX version as <see cref="DynamicsAx.AxVersions.ShortText"/> ("3.0", "2009", "2012 R2/R3" …), when <see cref="IsDynamicsAx"/>.</summary>
/// <summary>"3.0" (32-bit RECID) or "4.0+" (64-bit RECID), when <see cref="IsDynamicsAx"/>.</summary>
public string? DynamicsAxVersion { get; init; }
}

View file

@ -68,42 +68,15 @@ public sealed class DatabaseSelection
/// </summary>
public static class DynamicsAxFindings
{
/// <summary>The community AX rules (rules/community/dynamics-ax.json).</summary>
public const string RulePrefix = "COM-AX-";
/// <summary>The built-in AX rules (1.4: batch jobs, number sequences, RecId, AX users behind blocking).</summary>
public const string BuiltInRulePrefix = "SQLI-AX-";
private const string DatabaseSubject = "database: ";
/// <summary>Separates the database from the rest of an AX finding's subject: <c>database: AX30 · batch: 4711</c>.</summary>
public const string SubjectSeparator = " · ";
public static bool IsDynamicsAxRule(string ruleId) => ruleId?.StartsWith(RulePrefix, StringComparison.OrdinalIgnoreCase) == true;
public static bool IsDynamicsAxRule(string ruleId) =>
ruleId?.StartsWith(RulePrefix, StringComparison.OrdinalIgnoreCase) == true || ruleId?.StartsWith(BuiltInRulePrefix, StringComparison.OrdinalIgnoreCase) == true;
/// <summary>The subject of an AX finding about <paramref name="detail"/> in <paramref name="database"/>.</summary>
/// <remarks>
/// A middle dot in the database name is doubled, so the separator " · " (one dot) can only be the separator and a
/// database named e.g. "AX · test" can never be read as the database "AX" (whose hub opt-in would then apply).
/// </remarks>
public static string Subject(string database, string? detail = null)
{
ArgumentNullException.ThrowIfNull(database);
var escaped = database.Replace("·", "··", StringComparison.Ordinal);
return detail is null ? DatabaseSubject + escaped : DatabaseSubject + escaped + SubjectSeparator + detail;
}
/// <summary>The database a finding is about, from its subject (<c>database: AX30</c> or <c>database: AX30 · …</c>), or null.</summary>
public static string? DatabaseOf(string? subject)
{
if (subject is null || !subject.StartsWith(DatabaseSubject, StringComparison.OrdinalIgnoreCase) || subject.Length <= DatabaseSubject.Length)
{
return null;
}
var rest = subject[DatabaseSubject.Length..];
var separator = rest.IndexOf(SubjectSeparator, StringComparison.Ordinal);
return (separator > 0 ? rest[..separator] : rest).Replace("··", "·", StringComparison.Ordinal);
}
/// <summary>The database a finding is about, from its subject (<c>database: AX30</c>), or null.</summary>
public static string? DatabaseOf(string? subject) =>
subject is not null && subject.StartsWith(DatabaseSubject, StringComparison.OrdinalIgnoreCase) && subject.Length > DatabaseSubject.Length
? subject[DatabaseSubject.Length..]
: null;
}

View file

@ -52,12 +52,6 @@ public sealed partial record CustomRuleDefinition
/// <summary>Who wrote the rule (name or Forgejo user). Shown in the rule list.</summary>
public string? Author { get; init; }
/// <summary>
/// A Dynamics AX rule (scope Database): runs only in databases the central AX detection recognises (Core/DynamicsAx) and
/// that are chosen for monitoring, and its findings only reach SQLInsight Hub for databases where AX findings may be sent.
/// </summary>
public bool DynamicsAx { get; init; }
/// <summary>"community" for rules from the signed rule pack, "lokal" for the customer's own. Set when loaded.</summary>
[JsonIgnore]
public string Origin { get; init; } = "lokal";
@ -98,11 +92,6 @@ public sealed partial record CustomRuleDefinition
errors.Add($"minimumVersion '{MinimumVersion}' er ikke en version som 11.0.");
}
if (DynamicsAx && Scope != CustomRuleScope.Database)
{
errors.Add("dynamicsAx kræver scope Database.");
}
if (!string.IsNullOrWhiteSpace(Query))
{
errors.AddRange(QueryProblems(Query));
@ -112,10 +101,28 @@ public sealed partial record CustomRuleDefinition
}
/// <summary>
/// The query must be a single read-only SELECT (<see cref="ReadOnlyQuery"/>). This is a second line of defence: the
/// agent's login has no write permissions to begin with (grant-least-privilege.sql).
/// The query must be a single read-only SELECT. This is a second line of defence: the agent's login has no write
/// permissions to begin with (grant-least-privilege.sql).
/// </summary>
public static IEnumerable<string> QueryProblems(string query) => ReadOnlyQuery.Problems(query);
public static IEnumerable<string> QueryProblems(string query)
{
ArgumentNullException.ThrowIfNull(query);
var code = StringsAndComments().Replace(query, " ");
if (!StartsWithSelect().IsMatch(code))
{
yield return "Forespørgslen skal starte med SELECT eller WITH.";
}
foreach (Match forbidden in ForbiddenKeywords().Matches(code))
{
yield return $"Forespørgslen må ikke indeholde {forbidden.Value.ToUpperInvariant()}: regler må kun læse.";
}
if (code.TrimEnd().TrimEnd(';').Contains(';', StringComparison.Ordinal))
{
yield return "Forespørgslen må kun være én sætning (intet semikolon undervejs).";
}
}
/// <summary>Replaces {column} with the row's value; unknown placeholders are left as they are.</summary>
public static string Fill(string template, IReadOnlyDictionary<string, string?> row)
@ -128,6 +135,15 @@ public sealed partial record CustomRuleDefinition
[GeneratedRegex(@"^(COM|LOCAL)-[A-Z]{2,10}-[0-9]{3}$")]
private static partial Regex IdPattern();
[GeneratedRegex(@"'(?:[^']|'')*'|\[[^\]]*\]|""[^""]*""|--[^\n]*|/\*.*?\*/", RegexOptions.Singleline)]
private static partial Regex StringsAndComments();
[GeneratedRegex(@"^\s*(SELECT|WITH)\b", RegexOptions.IgnoreCase)]
private static partial Regex StartsWithSelect();
[GeneratedRegex(@"\b(INSERT|UPDATE|DELETE|MERGE|DROP|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE|GRANT|REVOKE|DENY|SHUTDOWN|KILL|BACKUP|RESTORE|DBCC|RECONFIGURE|OPENROWSET|OPENDATASOURCE|OPENQUERY|INTO|WAITFOR|USE|BULK|SETUSER)\b|\b(sp|xp)_\w+", RegexOptions.IgnoreCase)]
private static partial Regex ForbiddenKeywords();
[GeneratedRegex(@"\{([A-Za-z0-9_ ]+)\}")]
private static partial Regex Placeholder();
}
@ -229,6 +245,4 @@ public sealed record CustomQueryResults(IReadOnlyDictionary<string, CustomQueryR
/// <param name="Rows">Column name → value (as text) per row, when the query ran.</param>
/// <param name="Error">Why it did not run.</param>
/// <param name="PermissionDenied">The error was a missing permission (the rule is then not measurable, not failing).</param>
/// <param name="NotRelevant">Why the rule had nowhere to run (a Dynamics AX rule on an instance without monitored AX databases).</param>
public sealed record CustomQueryResult(IReadOnlyList<IReadOnlyDictionary<string, string?>> Rows, string? Error = null, bool PermissionDenied = false, bool Truncated = false,
string? NotRelevant = null);
public sealed record CustomQueryResult(IReadOnlyList<IReadOnlyDictionary<string, string?>> Rows, string? Error = null, bool PermissionDenied = false, bool Truncated = false);

View file

@ -1,186 +0,0 @@
using System.Text;
using System.Text.RegularExpressions;
namespace SQLInsight.Core.CustomRules;
/// <summary>
/// Whether a query someone typed (a custom rule, a synthetic SQL check) is one read-only SELECT. T-SQL is lexed the
/// way SQL Server reads it: nested block comments, <c>''</c> in strings, <c>]]</c> in [identifiers] and <c>""</c> in
/// "identifiers", so a keyword cannot hide in what only looks like a comment or a string. T-SQL needs no semicolon
/// between statements, so besides the writing keywords every statement keyword is refused, and a second SELECT is
/// only accepted after UNION, EXCEPT or INTERSECT.
/// This is a second line of defence: the login has no write permissions to begin with (grant-least-privilege.sql), and
/// a synthetic check also runs the query in a transaction that is always rolled back.
/// </summary>
public static partial class ReadOnlyQuery
{
public static IEnumerable<string> Problems(string query)
{
ArgumentNullException.ThrowIfNull(query);
var (code, unterminated) = Code(query);
if (unterminated is not null)
{
yield return $"Forespørgslen har {unterminated}, der ikke er afsluttet.";
yield break;
}
if (!StartsWithSelect().IsMatch(code))
{
yield return "Forespørgslen skal starte med SELECT eller WITH.";
}
foreach (var forbidden in ForbiddenKeywords().Matches(code).Select(m => Regex.Replace(m.Value.ToUpperInvariant(), @"\s+", " ")).Distinct(StringComparer.Ordinal))
{
yield return $"Forespørgslen må ikke indeholde {forbidden}: forespørgslen må kun læse (sæt [ ] om et kolonnenavn, der hedder sådan).";
}
if (code.TrimEnd().TrimEnd(';').Contains(';', StringComparison.Ordinal) || !OneStatement(code))
{
yield return "Forespørgslen må kun være én sætning (intet semikolon undervejs, og flere SELECT kun med UNION, EXCEPT eller INTERSECT).";
}
}
/// <summary>
/// The query with comments, strings and quoted identifiers replaced by a space, and what was left open (a comment,
/// a string, an identifier) or null.
/// </summary>
internal static (string Code, string? Unterminated) Code(string query)
{
var code = new StringBuilder(query.Length);
var i = 0;
while (i < query.Length)
{
var c = query[i];
var next = i + 1 < query.Length ? query[i + 1] : '\0';
if (c == '-' && next == '-')
{
var end = query.IndexOf('\n', i);
i = end < 0 ? query.Length : end;
code.Append(' ');
}
else if (c == '/' && next == '*')
{
// SQL Server nests block comments: /* /* */ is still inside the comment.
var depth = 0;
do
{
if (i + 1 < query.Length && query[i] == '/' && query[i + 1] == '*')
{
depth++;
i += 2;
}
else if (i + 1 < query.Length && query[i] == '*' && query[i + 1] == '/')
{
depth--;
i += 2;
}
else
{
i++;
}
}
while (depth > 0 && i < query.Length);
if (depth > 0)
{
return (code.ToString(), "en kommentar (/* … */)");
}
code.Append(' ');
}
else if (c is '\'' or '[' or '"')
{
var close = c == '[' ? ']' : c;
var j = i + 1;
while (true)
{
j = query.IndexOf(close, j);
if (j < 0)
{
return (code.ToString(), c == '\'' ? "en tekst ('…')" : "et navn i " + c + close);
}
if (j + 1 < query.Length && query[j + 1] == close)
{
j += 2; // '' or ]] or "": the quote itself, still inside
continue;
}
break;
}
i = j + 1;
code.Append(' ');
}
else
{
code.Append(c);
i++;
}
}
return (code.ToString(), null);
}
/// <summary>
/// One statement: the SELECTs outside parentheses are the query's own SELECT (for WITH, the one right after the
/// common table expressions) and those joined to it by UNION, EXCEPT or INTERSECT. "SELECT 1 SELECT 2" is two.
/// </summary>
private static bool OneStatement(string code)
{
var depth = 0;
var mainSeen = false;
var startsWithWith = StartsWithWith().IsMatch(code);
string? previous = null;
foreach (Match token in Tokens().Matches(code))
{
var value = token.Value;
if (value == "(")
{
depth++;
}
else if (value == ")")
{
depth = Math.Max(0, depth - 1);
}
else if (depth == 0 && value.Equals("SELECT", StringComparison.OrdinalIgnoreCase))
{
var joined = previous is not null && (previous.Equals("UNION", StringComparison.OrdinalIgnoreCase) || previous.Equals("ALL", StringComparison.OrdinalIgnoreCase)
|| previous.Equals("EXCEPT", StringComparison.OrdinalIgnoreCase) || previous.Equals("INTERSECT", StringComparison.OrdinalIgnoreCase));
var first = !mainSeen && (startsWithWith ? previous == ")" : previous is null);
if (!joined && !first)
{
return false;
}
mainSeen = true;
}
previous = value;
}
return true;
}
[GeneratedRegex(@"^\s*(SELECT|WITH)\b", RegexOptions.IgnoreCase)]
private static partial Regex StartsWithSelect();
[GeneratedRegex(@"^\s*WITH\b", RegexOptions.IgnoreCase)]
private static partial Regex StartsWithWith();
[GeneratedRegex(@"[()]|[A-Za-z_@#][A-Za-z0-9_@#$]*|[^\sA-Za-z_@#()]")]
private static partial Regex Tokens();
/// <summary>
/// Writing keywords, every other statement keyword (T-SQL needs no semicolon between statements), sequences
/// (NEXT VALUE FOR is not rolled back) and table hints that take update or exclusive locks.
/// </summary>
[GeneratedRegex(
@"\b(INSERT|UPDATE|DELETE|MERGE|DROP|ALTER|CREATE|TRUNCATE|EXEC|EXECUTE|GRANT|REVOKE|DENY|SHUTDOWN|KILL|BACKUP|RESTORE|DBCC|RECONFIGURE"
+ @"|OPENROWSET|OPENDATASOURCE|OPENQUERY|INTO|WAITFOR|USE|BULK|SETUSER"
+ @"|WRITETEXT|UPDATETEXT|READTEXT|CHECKPOINT|COMMIT|ROLLBACK|SAVE|BEGIN|TRAN|TRANSACTION|DECLARE|SET|PRINT|RAISERROR|THROW|IF|WHILE|GOTO|RETURN"
+ @"|BREAK|CONTINUE|OPEN|CLOSE|DEALLOCATE|REVERT|ADD|ENABLE|DISABLE|RECEIVE|SEND|CONVERSATION|DIALOG|LOAD|DUMP"
+ @"|UPDLOCK|XLOCK|TABLOCK|TABLOCKX|HOLDLOCK|PAGLOCK|SERIALIZABLE|REPEATABLEREAD)\b|\bNEXT\s+VALUE\s+FOR\b|\b(sp|xp)_\w+",
RegexOptions.IgnoreCase)]
private static partial Regex ForbiddenKeywords();
}

View file

@ -45,10 +45,4 @@ public sealed record SessionInfo
public string? SqlText { get; init; }
public DateTime? LastRequestEndTime { get; init; }
/// <summary>
/// The session's CONTEXT_INFO as hex, without trailing zero bytes, or null when not set. Dynamics AX 2009/2012 AOS
/// can put the AX user and session there (Core/DynamicsAx/AxSessionMapper.cs). Sensitive: it can hold a user id.
/// </summary>
public string? ContextInfo { get; init; }
}

View file

@ -1,226 +0,0 @@
using System.Text.Json.Serialization;
namespace SQLInsight.Core.DynamicsAx;
/// <summary>A batch job's status, translated from the version's STATUS enum by its adapter.</summary>
public enum AxBatchStatus
{
Unknown,
Hold,
Waiting,
Executing,
Error,
Finished,
Ready,
NotRun,
Cancelling,
Canceled,
}
/// <summary>
/// A batch job row as read from AX, before interpretation: dates and times exactly as stored. AX 3.0/4.0 store a local
/// date (time part 00:00) plus seconds since midnight; AX 2009 and later a UTC datetime and no seconds.
/// </summary>
/// <param name="ServerUtcOffsetMinutes">DATEDIFF(minute, GETDATE(), GETUTCDATE()) on the server when read: turns local AX times into UTC.</param>
public sealed record AxBatchJobRow(
long RecId,
int? ClassNumber,
string? Caption,
string? Company,
string? Group,
int Status,
DateTime? PlannedDate,
int? PlannedSeconds,
DateTime? StartDate,
int? StartSeconds,
DateTime? EndDate,
int? EndSeconds,
int ServerUtcOffsetMinutes);
/// <summary>A finished run from AX's batch history (BATCHJOBHISTORY, 2009 and later).</summary>
public sealed record AxBatchHistoryRow(
long RecId,
long? JobRecId,
string? Caption,
string? Company,
int Status,
DateTime? StartDate,
DateTime? EndDate,
int ServerUtcOffsetMinutes);
/// <summary>A completed run of a batch job: from AX's history, or seen by SQLInsight itself. Stored in insight.AxBatchRuns.</summary>
/// <param name="RunKey">Unique per database, so a run seen twice is stored once.</param>
public sealed record AxBatchRun(string JobKey, string RunKey, string? Caption, string? Company, DateTime StartedUtc, DateTime EndedUtc, AxBatchStatus Status)
{
[JsonIgnore]
public TimeSpan Duration => EndedUtc - StartedUtc;
}
/// <summary>A batch job now, with what its own history says is normal.</summary>
public sealed record AxBatchJob
{
/// <summary>Stable per job: the BATCHJOB RECID (2009 and later), or class, company and group (3.0/4.0).</summary>
public required string Key { get; init; }
public long RecId { get; init; }
public required string Caption { get; init; }
public string? Company { get; init; }
public string? Group { get; init; }
public int StatusCode { get; init; }
public AxBatchStatus Status { get; init; }
public DateTime? PlannedStartUtc { get; init; }
public DateTime? StartedUtc { get; init; }
public DateTime? EndedUtc { get; init; }
/// <summary>When SQLInsight first saw the job executing (AX 3.0/4.0 do not store the actual start of a running job).</summary>
public DateTime? FirstSeenExecutingUtc { get; init; }
/// <summary>Completed runs in SQLInsight's history of the job (at most <see cref="AxBatchHistory.RunsKept"/>).</summary>
public int HistoryRuns { get; init; }
public double? MedianSeconds { get; init; }
public double? P90Seconds { get; init; }
public double? LastRunSeconds { get; init; }
public AxBatchStatus? LastRunStatus { get; init; }
public DateTime? LastRunEndedUtc { get; init; }
/// <summary>When the job started running, as well as SQLInsight knows it.</summary>
[JsonIgnore]
public DateTime? RunningSince => Status == AxBatchStatus.Executing ? StartedUtc ?? FirstSeenExecutingUtc : null;
public TimeSpan? Running(DateTimeOffset now) => RunningSince is { } since ? now.UtcDateTime - since : null;
/// <summary>How late a waiting job is: now minus its planned start, when positive.</summary>
public TimeSpan? Late(DateTimeOffset now) =>
Status == AxBatchStatus.Waiting && PlannedStartUtc is { } planned && now.UtcDateTime > planned ? now.UtcDateTime - planned : null;
}
/// <summary>Batch jobs in one AX database, or why they could not be read.</summary>
public sealed record AxBatchDatabase
{
public required string Database { get; init; }
public AxVersion Version { get; init; }
/// <summary>Null when measured; else "Ikke målbar: …" with the exact GRANT text when it is the read access.</summary>
public string? NotMeasurable { get; init; }
public IReadOnlyList<AxBatchJob> Jobs { get; init; } = [];
/// <summary>Runs completed since the previous collection (stored in insight.AxBatchRuns).</summary>
public IReadOnlyList<AxBatchRun> NewRuns { get; init; } = [];
/// <summary>
/// How many jobs had each raw STATUS value: shows whether the version's status mapping fits the customer's data
/// (the AX 3.0 mapping is not verified, see docs/dynamics-ax.md).
/// </summary>
public IReadOnlyDictionary<int, int> StatusCounts { get; init; } = new Dictionary<int, int>();
/// <summary>Optional columns missing and what that leaves out.</summary>
public IReadOnlyList<string> Notes { get; init; } = [];
/// <summary>The highest BATCHJOBHISTORY RECID read: the next read starts there (2009 and later).</summary>
public long? HistoryWatermark { get; init; }
/// <summary>When the batch tables were last read (a lock timeout keeps the earlier picture for a while, see AxBatchCollector).</summary>
public DateTimeOffset? MeasuredAt { get; init; }
}
/// <summary>The batch jobs of every monitored AX database on an instance (collector "dynamics-ax-batch", every 5 minutes).</summary>
public sealed record AxBatchSnapshot
{
public required DateTimeOffset CapturedAt { get; init; }
public required AxThresholds Thresholds { get; init; }
public required IReadOnlyList<AxBatchDatabase> Databases { get; init; }
/// <summary>
/// Durations of recent runs per database and job, for comparing a job with itself. Kept in memory and rebuilt from
/// insight.AxBatchRuns after a restart; not part of the stored snapshot.
/// </summary>
[JsonIgnore]
public AxBatchHistory History { get; init; } = new();
/// <summary>When each running job was first seen executing, per database and job. Kept in memory.</summary>
[JsonIgnore]
public IReadOnlyDictionary<string, DateTime> FirstSeenExecuting { get; init; } = new Dictionary<string, DateTime>();
/// <summary>Run keys stored lately, so a run read again is not stored twice. Kept in memory.</summary>
[JsonIgnore]
public IReadOnlySet<string> RecentRunKeys { get; init; } = new HashSet<string>();
}
/// <summary>Recent run durations per database and job; bounded to <see cref="RunsKept"/> runs per job.</summary>
public sealed class AxBatchHistory
{
public const int RunsKept = 20;
private readonly Dictionary<string, List<AxBatchRun>> _runs = new(StringComparer.OrdinalIgnoreCase);
public static string Key(string database, string jobKey) => database + "\u001f" + jobKey;
/// <summary>A run key made unique across the instance's databases (<see cref="AxBatchSnapshot.RecentRunKeys"/>).</summary>
public static string ScopedRunKey(string database, string runKey) => database + "\u001f" + runKey;
/// <summary>The scoped run keys of every run kept.</summary>
public IEnumerable<string> ScopedRunKeys() =>
_runs.SelectMany(kv => kv.Value.Select(r => ScopedRunKey(kv.Key[..kv.Key.IndexOf('\u001f', StringComparison.Ordinal)], r.RunKey)));
public void Add(string database, AxBatchRun run)
{
ArgumentNullException.ThrowIfNull(run);
var key = Key(database, run.JobKey);
if (!_runs.TryGetValue(key, out var list))
{
_runs[key] = list = [];
}
if (list.Any(r => r.RunKey == run.RunKey))
{
return;
}
list.Add(run);
list.Sort((a, b) => a.EndedUtc.CompareTo(b.EndedUtc));
if (list.Count > RunsKept)
{
list.RemoveRange(0, list.Count - RunsKept);
}
}
/// <summary>The job's runs, oldest first.</summary>
public IReadOnlyList<AxBatchRun> Runs(string database, string jobKey) =>
_runs.TryGetValue(Key(database, jobKey), out var list) ? list : [];
public int Count => _runs.Count;
/// <summary>A copy, so the previous snapshot is never changed.</summary>
public AxBatchHistory Clone()
{
var copy = new AxBatchHistory();
foreach (var (key, list) in _runs)
{
copy._runs[key] = [.. list];
}
return copy;
}
/// <summary>Median and 90th percentile of the successful runs' durations, in seconds.</summary>
public static (double? Median, double? P90) Statistics(IEnumerable<AxBatchRun> runs)
{
var seconds = runs.Where(r => r.Status == AxBatchStatus.Finished).Select(r => r.Duration.TotalSeconds).Where(s => s >= 0).Order().ToList();
if (seconds.Count == 0)
{
return (null, null);
}
return (Percentile(seconds, 0.5), Percentile(seconds, 0.9));
}
private static double Percentile(List<double> sorted, double p)
{
var position = p * (sorted.Count - 1);
var lower = (int)Math.Floor(position);
var upper = Math.Min(lower + 1, sorted.Count - 1);
return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
}
}

View file

@ -1,99 +0,0 @@
namespace SQLInsight.Core.DynamicsAx;
/// <summary>A column of an AX table as sys.columns describes it.</summary>
public sealed record AxColumn(string Name, string Type, int Precision, int MaxLength);
/// <param name="Readable">The agent has SELECT on the table (the optional AX read access, grant-dynamics-ax-read.sql).</param>
public sealed record AxTable(string Name, bool Readable, IReadOnlyList<AxColumn> Columns)
{
public bool HasColumn(string column) => Columns.Any(c => string.Equals(c.Name, column, StringComparison.OrdinalIgnoreCase));
public AxColumn? Column(string column) => Columns.FirstOrDefault(c => string.Equals(c.Name, column, StringComparison.OrdinalIgnoreCase));
}
/// <summary>A company (DATAAREA.ID) in an AX database.</summary>
public sealed record AxCompany(string Id, string? Name, bool IsVirtual);
/// <summary>
/// A database recognised as Dynamics AX by the central detection (<see cref="AxDetection"/>): which version, which
/// schema owns the AX tables (bmssa in AX 3.0, dbo later), the AX tables SQLInsight knows with their columns and
/// whether the agent may read them, and the companies when DATAAREA is readable.
/// </summary>
public sealed record AxDatabase
{
public required string Database { get; init; }
/// <summary>The schema of the AX tables: bmssa (AX 3.0, usually), dbo (later).</summary>
public required string Schema { get; init; }
public required AxVersion Version { get; init; }
/// <summary>The AX tables SQLInsight looks at (<see cref="AxDetection.KnownTables"/>) found in <see cref="Schema"/>.</summary>
public IReadOnlyList<AxTable> Tables { get; init; } = [];
/// <summary>The separate model store database (&lt;database&gt;_model) of AX 2012 R2/R3, when it is on the instance.</summary>
public string? ModelDatabase { get; init; }
/// <summary>The companies (DATAAREA), or null when the agent may not read DATAAREA.</summary>
public IReadOnlyList<AxCompany>? Companies { get; init; }
/// <summary>Why <see cref="Companies"/> is null.</summary>
public string? CompaniesReason { get; init; }
/// <summary>The database user the agent's login is in this database, or null when it has none (CONNECT ANY DATABASE).</summary>
public string? DatabaseUser { get; init; }
/// <summary>The agent's login (SUSER_SNAME()), so the GRANT text names it exactly.</summary>
public string? AgentLogin { get; init; }
/// <summary>Reads use snapshot isolation (ALLOW_SNAPSHOT_ISOLATION) or read committed snapshot; else read committed with a short lock timeout.</summary>
public AxReadIsolation Isolation { get; init; } = AxReadIsolation.ReadCommitted;
/// <summary>How the version was decided, and anything uncertain about it (Danish).</summary>
public IReadOnlyList<string> Notes { get; init; } = [];
public string VersionText => AxVersions.Text(Version);
public bool Verified => AxVersions.IsVerified(Version);
public AxTable? Table(string name) => Tables.FirstOrDefault(t => string.Equals(t.Name, name, StringComparison.OrdinalIgnoreCase));
public bool HasTable(string name) => Table(name) is not null;
public bool HasColumn(string table, string column) => Table(table)?.HasColumn(column) == true;
public bool CanRead(string table) => Table(table)?.Readable == true;
/// <summary>[schema].[TABLE], quoted.</summary>
public string Qualified(string table) => AxSql.Identifier(Schema) + "." + AxSql.Identifier(table);
}
public enum AxReadIsolation
{
/// <summary>Read committed with SET LOCK_TIMEOUT: a locked row makes the read give up after a few seconds, never wait.</summary>
ReadCommitted,
/// <summary>The database has READ_COMMITTED_SNAPSHOT on: read committed reads versions and never waits for locks.</summary>
ReadCommittedSnapshot,
/// <summary>ALLOW_SNAPSHOT_ISOLATION is on: reads use SET TRANSACTION ISOLATION LEVEL SNAPSHOT.</summary>
Snapshot,
}
/// <summary>The AX databases on an instance, as the detector last saw them (collector "dynamics-ax", hourly).</summary>
public sealed record AxDatabaseSnapshot
{
public required DateTimeOffset CapturedAt { get; init; }
/// <summary>Recognised AX databases that are monitored (chosen in Servere og databaser).</summary>
public required IReadOnlyList<AxDatabase> Databases { get; init; }
/// <summary>The agent's login (SUSER_SNAME()), for the GRANT text.</summary>
public string? AgentLogin { get; init; }
/// <summary>Databases the detection could not look into, with why (Danish).</summary>
public IReadOnlyDictionary<string, string> Problems { get; init; } = new Dictionary<string, string>();
public AxDatabase? Find(string? database) =>
database is null ? null : Databases.FirstOrDefault(d => string.Equals(d.Database, database, StringComparison.OrdinalIgnoreCase));
}

View file

@ -1,154 +0,0 @@
namespace SQLInsight.Core.DynamicsAx;
/// <summary>One row of the detection query: a column of a known AX table.</summary>
public sealed record AxDetectionRow(string Schema, string Table, string Column, string Type, int Precision, int MaxLength, bool Readable, string? DatabaseUser = null);
/// <summary>
/// The one place Dynamics AX is recognised: discovery (Servere og databaser), the AX collectors, the AX community
/// rules and the web UI all use it. Reads metadata only (sys.tables, sys.columns, HAS_PERMS_BY_NAME), never AX data.
/// <para>
/// A database is AX when SQLDICTIONARY and DATAAREA are in the same schema. The version follows from the schema:
/// RECID int (precision 10) → Axapta 2.5/3.0; bigint without BATCHJOB → AX 4.0; BATCHJOB without NUMBERSEQUENCESCOPE →
/// AX 2009; NUMBERSEQUENCESCOPE without a PARTITION column → AX 2012 RTM/FP; PARTITION with the model store database
/// &lt;db&gt;_model on the instance → AX 2012 R2/R3; PARTITION without any model store → Dynamics 365 F&amp;O (formodet).
/// Only the AX 3.0 rules are verified against a real customer schema; the rest follow Microsoft's documentation.
/// </para>
/// </summary>
public static class AxDetection
{
/// <summary>The AX tables SQLInsight knows: the markers and every table an AX check reads.</summary>
public static readonly IReadOnlyList<string> KnownTables =
[
"SQLDICTIONARY", "DATAAREA", "SYSCONFIG",
"BATCH", "BATCHJOB", "BATCHJOBHISTORY", "BATCHGROUP",
"NUMBERSEQUENCETABLE", "NUMBERSEQUENCESCOPE", "NUMBERSEQUENCEREFERENCE",
"SYSTEMSEQUENCES", "MODELELEMENT",
];
private static string TableList => string.Join(", ", KnownTables.Select(t => "N'" + t + "'"));
/// <summary>
/// Run inside the database (<see cref="AxSql.InDatabase"/>), SQL Server 2005 and later: every column of the known AX
/// tables, whether the agent may SELECT from the table, and the agent's database user. Names are upper-cased, so a
/// case-sensitive collation does not matter.
/// </summary>
public static string MetadataSql => $"""
SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length,
ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N'.' + QUOTENAME(t.name), N'OBJECT', N'SELECT'), 0),
(SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID())
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
JOIN sys.columns c ON c.object_id = t.object_id
WHERE UPPER(t.name) IN ({TableList})
ORDER BY s.name, t.name, c.column_id
""";
/// <summary>
/// SQL Server 2000 (legacy ODBC), with a three-part name so the session never changes database. No permission
/// information: the AX data checks need SQL Server 2005 or later.
/// </summary>
public static string MetadataSql2000(string database) => $"""
SELECT u.name, UPPER(o.name), UPPER(c.name), TYPE_NAME(c.xtype), c.xprec, c.length, 0, NULL
FROM {AxSql.Identifier(database)}.dbo.sysobjects o
JOIN {AxSql.Identifier(database)}.dbo.sysusers u ON u.uid = o.uid
JOIN {AxSql.Identifier(database)}.dbo.syscolumns c ON c.id = o.id
WHERE o.xtype = 'U' AND UPPER(o.name) IN ({TableList})
ORDER BY u.name, o.name, c.colid
""";
/// <summary>
/// Decides from a database's detection rows whether it is AX, and which version.
/// </summary>
/// <param name="databasesOnInstance">Every database name on the instance, to find the AX 2012 R2/R3 model store (&lt;db&gt;_model).</param>
public static AxDatabase? Recognise(string database, IReadOnlyList<AxDetectionRow> rows, IEnumerable<string> databasesOnInstance)
{
ArgumentNullException.ThrowIfNull(database);
ArgumentNullException.ThrowIfNull(rows);
ArgumentNullException.ThrowIfNull(databasesOnInstance);
var schemas = rows
.GroupBy(r => r.Schema, StringComparer.OrdinalIgnoreCase)
.Select(g => (Schema: g.Key, Tables: g.GroupBy(r => r.Table, StringComparer.OrdinalIgnoreCase)
.Select(t => new AxTable(t.Key.ToUpperInvariant(), t.Any(r => r.Readable),
t.Select(r => new AxColumn(r.Column.ToUpperInvariant(), r.Type, r.Precision, r.MaxLength)).ToList()))
.ToList()))
.Where(s => s.Tables.Any(t => t.Name == "SQLDICTIONARY") && s.Tables.Any(t => t.Name == "DATAAREA"))
.OrderByDescending(s => s.Tables.Count)
.ThenBy(s => string.Equals(s.Schema, "dbo", StringComparison.OrdinalIgnoreCase) ? 0 : 1)
.ToList();
if (schemas.Count == 0)
{
return null;
}
var (schema, tables) = schemas[0];
var notes = new List<string>();
bool Has(string table) => tables.Any(t => t.Name == table);
var recId = tables.Single(t => t.Name == "SQLDICTIONARY").Column("RECID");
var partition = tables.Any(t => t.Name != "SQLDICTIONARY" && t.HasColumn("PARTITION"));
var modelDatabase = databasesOnInstance.FirstOrDefault(d => string.Equals(d, database + "_model", StringComparison.OrdinalIgnoreCase));
AxVersion version;
if (recId is null)
{
version = AxVersion.Unknown;
notes.Add("SQLDICTIONARY har ingen RECID-kolonne, så versionen kan ikke bestemmes. AX-datatjek køres ikke.");
}
else if (recId.Precision <= 10)
{
version = AxVersion.Axapta30;
notes.Add($"RECID er 32-bit (int) i skemaet {schema}: Axapta 2.5 eller 3.0 (de to kan ikke skelnes ud fra skemaet).");
}
else if (!Has("BATCHJOB"))
{
version = AxVersion.Ax40;
notes.Add("RECID er 64-bit, og BATCHJOB findes ikke: AX 4.0.");
}
else if (!Has("NUMBERSEQUENCESCOPE"))
{
version = AxVersion.Ax2009;
notes.Add("BATCHJOB findes, NUMBERSEQUENCESCOPE gør ikke: AX 2009.");
}
else if (!partition)
{
version = AxVersion.Ax2012;
notes.Add(Has("MODELELEMENT")
? "NUMBERSEQUENCESCOPE uden PARTITION-kolonne, og modellageret ligger i selve databasen: AX 2012 RTM/FP."
: "NUMBERSEQUENCESCOPE uden PARTITION-kolonne: AX 2012 RTM/FP.");
}
else if (modelDatabase is not null || Has("MODELELEMENT"))
{
version = AxVersion.Ax2012R2;
notes.Add(modelDatabase is not null
? $"PARTITION-kolonne og modeldatabasen {modelDatabase}: AX 2012 R2/R3."
: "PARTITION-kolonne og modellager i databasen: AX 2012 R2/R3.");
}
else
{
version = AxVersion.FinanceOperations;
notes.Add($"PARTITION-kolonne, men intet modellager ({database}_model findes ikke på instansen): formodet Dynamics 365 F&O on-premises. "
+ "Ligger AX 2012 R2/R3-modeldatabasen på en anden instans, er det i stedet AX 2012 R2/R3.");
}
if (!Has("SYSCONFIG"))
{
notes.Add("SYSCONFIG findes ikke (genkendt på SQLDICTIONARY og DATAAREA).");
}
if (!AxVersions.IsVerified(version) && version != AxVersion.Unknown)
{
notes.Add(AxVersions.VerificationText(version));
}
return new AxDatabase
{
Database = database,
Schema = schema,
Version = version,
Tables = tables,
ModelDatabase = modelDatabase,
DatabaseUser = rows.FirstOrDefault(r => r.DatabaseUser is not null)?.DatabaseUser,
Notes = notes,
};
}
}

View file

@ -1,73 +0,0 @@
namespace SQLInsight.Core.DynamicsAx;
/// <summary>
/// Axapta 3.0 and AX 4.0 store batch and cleanup times as a local date plus seconds since midnight, in the time of the
/// machine that wrote them (the AOS, or the batch client in a 2-tier installation). They become UTC with the rules of
/// that time zone on that date, so a time from before a change between summer and winter time is not an hour off.
/// <list type="bullet">
/// <item>The zone is SQLInsight:DynamicsAx:LocalTimeZone when set (a Windows or IANA id), else the agent's own zone when
/// its offset right now is the SQL Server's (the usual case: agent, SQL Server and AOS in one country).</item>
/// <item>Otherwise the SQL Server's current offset is used for every time, with a note that times from the other side of a
/// change can be an hour off.</item>
/// <item>A local time that does not exist (the hour skipped in spring) or exists twice (the hour repeated in autumn) gets
/// the zone's standard offset: deterministic, never an exception, and in autumn the later of the two moments, so a job
/// is never reported late because of the repeated hour.</item>
/// </list>
/// </summary>
public static class AxLocalTime
{
/// <summary>The setting that names the AX time zone.</summary>
public const string Setting = "SQLInsight:DynamicsAx:LocalTimeZone";
/// <summary>
/// A local AX date and seconds since midnight as UTC. AX's empty date (1900-01-01 or earlier) and seconds outside
/// 0..86400 are unknown (null); 86400 is midnight at the end of the day.
/// </summary>
/// <param name="serverOffsetMinutes">DATEDIFF(minute, GETDATE(), GETUTCDATE()) on the SQL Server when read (UTC − local).</param>
public static DateTime? ToUtc(DateTime? date, int? seconds, int serverOffsetMinutes, TimeZoneInfo? zone)
{
if (date is not { } d || d.Year <= 1900 || seconds is < 0 or > 86_400)
{
return null;
}
var local = DateTime.SpecifyKind(d.Date.AddSeconds(seconds ?? 0), DateTimeKind.Unspecified);
var offset = zone is null ? TimeSpan.FromMinutes(-RoundedOffset(serverOffsetMinutes)) : zone.GetUtcOffset(local);
return DateTime.SpecifyKind(local - offset, DateTimeKind.Utc);
}
/// <summary>
/// The SQL Server's offset to the nearest quarter of an hour: GETDATE() and GETUTCDATE() are two calls, and
/// DATEDIFF(minute) counts minute boundaries, so the raw value can be a minute off.
/// </summary>
public static int RoundedOffset(int minutes) => (int)Math.Round(minutes / 15.0, MidpointRounding.AwayFromZero) * 15;
/// <summary>
/// The zone for a database's local AX times, or null to use the SQL Server's current offset, and a note (Danish) when
/// that is less exact than it could be.
/// </summary>
/// <param name="configured">SQLInsight:DynamicsAx:LocalTimeZone.</param>
/// <param name="serverOffsetMinutes">The SQL Server's offset (UTC − local) when read, or null when nothing was read.</param>
/// <param name="agentZone">The agent's own zone (TimeZoneInfo.Local; a parameter for tests).</param>
public static (TimeZoneInfo? Zone, string? Note) Resolve(string? configured, int? serverOffsetMinutes, DateTime nowUtc, TimeZoneInfo? agentZone = null)
{
if (!string.IsNullOrWhiteSpace(configured))
{
return TimeZoneInfo.TryFindSystemTimeZoneById(configured.Trim(), out var zone)
? (zone, null)
: (null, $"Tidszonen \"{configured.Trim()}\" ({Setting}) findes ikke på agentens server. AX' lokale tider omregnes i stedet med SQL Servers "
+ "nuværende forskel til UTC, så tider fra før et skift mellem sommer- og vintertid kan være en time forkerte.");
}
agentZone ??= TimeZoneInfo.Local;
var now = DateTime.SpecifyKind(nowUtc, DateTimeKind.Utc);
if (serverOffsetMinutes is not { } server || -(int)Math.Round(agentZone.GetUtcOffset(now).TotalMinutes) == RoundedOffset(server))
{
return (agentZone, null);
}
return (null, $"AX' lokale tider (Axapta 3.0/4.0) omregnes med SQL Servers nuværende forskel til UTC, fordi agentens tidszone ({agentZone.Id}) "
+ $"har en anden forskel lige nu. Tider fra før et skift mellem sommer- og vintertid kan derfor være en time forkerte. Sæt {Setting} "
+ "til AOS'ens tidszone for at få dem helt rigtige.");
}
}

Some files were not shown because too many files have changed in this diff Show more