Compare commits

...

47 commits
v1.3.0 ... main

Author SHA1 Message Date
29c0fb439d Version 1.5.2
Some checks failed
deploy community / deploy (push) Successful in 20s
deploy hub / deploy (push) Successful in 22s
CI / Build og unit tests (push) Successful in 12s
CI / MSI-installer (push) Successful in 14s
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Has been cancelled
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Has been cancelled
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Has been cancelled
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 09:43:05 +02:00
8db226d2b6 Dynamics AX 3.0: batch STATUS checked against customer data
Cascorp's BATCH table: every STATUS 4 row has an end time and no exception
code (finished), STATUS 1 rows have no end time and starts long passed
(waiting). So AX 3.0 uses the same order as AX 4.0+: 0 Tilbageholdt,
1 Venter, 2 Udfører, 3 Fejl, 4 Afsluttet. The old assumption (0 Venter ...
4 Tilbageholdt) treated finished jobs as held and waiting ones as running.

SQLI-AX-003 skips waiting jobs planned more than BatchAbandonedDays (30)
ago: the customer has 25 such rows from 2007-2018 that no batch server
serves; they are dead rows, not a live alarm.

Adds docs/dynamics-ax/verificer-ax30-batchstatus.sql (read-only) to repeat
the check at other AX 3.0 customers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 09:36:56 +02:00
b878fd3ab6 Version 1.5.1: Windows-only tests pass in the Linux CI build
Some checks failed
deploy community / deploy (push) Successful in 16s
deploy hub / deploy (push) Successful in 18s
CI / Build og unit tests (push) Successful in 9s
CI / MSI-installer (push) Successful in 15s
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Has been cancelled
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Has been cancelled
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Has been cancelled
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Has been cancelled
- AxLogonClient.Command always builds a Windows path to powershell.exe
- The grant-script test is skipped outside Windows (needs Windows PowerShell)
- The DNS test looks up an absolute name, so a search domain can't resolve it

v1.5.0 was tagged but never released because these failed in CI.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 16:49:05 +02:00
a3af1334e7 Version 1.5.0
Some checks failed
CI / MSI-installer (push) Has been skipped
deploy community / deploy (push) Successful in 14s
deploy hub / deploy (push) Successful in 18s
CI / Build og unit tests (push) Failing after 39s
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Has been cancelled
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Has been cancelled
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Has been cancelled
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 16:06:36 +02:00
b29ebb424d Review 1.5: docs and appsettings for the reviewed behaviour: timeouts and backoff, new keys, API paging, dedup and redaction
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 16:06:22 +02:00
b010f37dcf Review 1.5: synthetic checks: "Kør nu" joins the running check, errors are stored once, paused checks are left alone
- "Kør nu" (ServerRequestHandler) goes through SyntheticCheckService.RunNowAsync: it joins a
  run of the check that is going on, or runs it under the same MaxParallelChecks throttle
  and registers it, so it never overlaps a scheduled run. A paused check is not run: the
  answer is "Tjekket er sat på pause." and nothing is stored, evaluated or opened.
- Server requests (test, run now) may take 3 min (the check's max timeout 120 s + the
  30 s service lookup + 30 s margin) instead of AgentRequestService's 2 min, so the result
  is always stored; a server test gets 2.5 min. Stale Running requests are cleaned after
  the longest timeout + 1 min.
- An exception a check run did not foresee becomes a stored failed result ("Tjekket kunne
  ikke køres: …"), so LastRunAt advances and the check waits its interval; it is logged
  once per change. A run that cannot even be stored is not retried before its interval
  (the agent remembers when it started it).
- The service lookup of a failing check has its own 30 s deadline; its error detail
  (naming the agent's account) is Sensitive.
- The rules get max(10, FailuresBeforeFinding, SlowRunsBeforeFinding) + 1 recent results
  (at most 100), so "Fejlet i træk" and FailuresBeforeFinding > 10 work.
- The interval is never under SyntheticCheckLimits.MinIntervalSeconds agent-side; state
  for removed checks is pruned; shutdown waits for running checks before the throttle is
  disposed; a TaskCanceledException that is not shutdown no longer ends the service.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 16:02:17 +02:00
6d8a5d9ad7 Review 1.5: a Windows server that hangs no longer holds up the agent; reads are bounded and backed off
Deadlines
- Event log over RPC (MS-EVEN6): the whole read has a deadline (OperationTimeout per log);
  the token cancels the EventLogSession's operations and is checked before each
  FormatDescription, and the agent stops waiting even if the native call ignores it.
  Reported as ServerAccessProblem.Timeout ("svarede ikke i tide over hændelsesloggens
  protokol (RPC) inden for N sekunder").
- Every CIM call runs on the thread pool and is awaited with the caller's token; a call
  that ignores cancellation is abandoned and the CIM session is closed once it returns.
- Each server cycle has a deadline (new SQLInsight:Servers:CycleTimeout, default 5 min,
  1-30 min, at least 2 x OperationTimeout): the areas not read by then are "ikke målbar"
  ("Serveren svarede ikke inden for N minutter, så indsamlingen blev afbrudt; …"),
  logged once, and the slot is freed.
- Scheduled tasks: run information is read within a time budget (CycleTimeout / 3); the
  rest keep their state without the last run, and the coverage note says so.
- "Test server" has an overall deadline (WindowsServerTester, default 2 min); areas not
  done in time are reported as not answered, so the result is always stored.

Backoff and memory
- A server that does not answer (Unreachable/Timeout, or a timed-out cycle) is backed
  off 1, 2, 5, 10, then 15 minutes, reset when it answers.
- Protocol Auto: the protocol that worked (e.g. DCOM after WS-Man was refused, also for
  access denied) is tried first for an hour, then WS-Man is tried again; a server whose
  event log RPC failed is read over CIM for an hour.
- Over WS-Man with read-only WinRM rights Task Scheduler's GetInfoByName is denied: with
  Auto the tasks are read over DCOM instead (remembered for the server); with WSMan forced
  the reason says to choose Auto or DCOM. The advice "winrm configSDDL default" (broader
  WinRM rights) is gone; texts follow the grant script (-WinRM read right,
  -EnableFirewall -AgentAddress), not Remote Management Users.

Events
- Each log continues from its own mark (ServerEventSnapshot.LogReadUntil, a small additive
  field in Core): a truncated System log no longer loses the events between its last
  read event and the newest Application event.
- CIM fallback: Win32_NTLogEvent is streamed and only the oldest max per log are kept
  (memory bounded, deterministic order), consistent with the RPC read and the coverage
  text, which now names the logs that had more.

Other
- CimInstances (and method results) are mapped to SQLInsight's types and disposed at once.
- Options are clamped when bound (ServerOptions/SyntheticCheckOptions.Normalize):
  OperationTimeout 5 s-5 min, MaxEventsPerCollection 10-10000, MaxWatched 1-200,
  MaxParallelServers/Checks 1-32, retention days >= 1, ForecastHistoryDays 1-365,
  FailuresBeforeFinding/SlowRunsBeforeFinding 1-99.
- Shutdown: the timer's cancellation is caught, running cycles are awaited before the
  throttle is disposed; a cycle's time is taken when it gets its slot; a TaskCanceledException
  that is not shutdown no longer ends the service.
- Retention maintenance also runs (hourly) when SQLInsight:Servers:Enabled is false but
  synthetic checks are on.
- LocalCimTests run alone (collection without parallelization), share one warmed-up
  session, use the same 2 min operation timeout and a 5 min deadline per test, a smaller
  workload, and report timings in assertion messages.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 16:02:17 +02:00
f980bd3a4a Review 1.5: synthetic check runner: network surprises are results, AX process never outlives the agent
- An HTTP answer that breaks off while it is read (HttpIOException/IOException) is a
  failed check ("Svaret blev afbrudt, mens det blev læst: …"), not an exception that
  made the check rerun every tick. Other unforeseen network errors (IOException,
  SocketException, HttpRequestException, AuthenticationException) become
  "Forbindelsen fejlede: …"; one that hits the check's deadline is "Intet svar inden
  for N sekunder". DNS: an invalid name (ArgumentException) is a failed lookup.
- The HTTP "Adresse" detail (a URL may carry query secrets) and the AX "Object server"
  detail are Sensitive, so the API redacts them.
- SyntheticCheckRun's compiler-generated ToString no longer prints the SQL password.
- ProcessRunner (AX Business Connector logon): the PowerShell process tree is killed in
  a finally whenever it has not exited, also when the agent stops (before: only on the
  check's timeout, leaving an orphaned process and AX session). Output and errors are
  capped at 64 K characters each; the rest is read and dropped so the process never
  blocks on a full pipe.
- Tests: broken body, password redaction, bounded output, killed on agent stop.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 16:01:47 +02:00
37d3d4b5ec Review 1.5: a check sent back from API v1 with [udeladt] login or URL query is refused, not saved
GET returns the SQL login and the URL query redacted; a client that PUTs it back unchanged
would otherwise replace the real values.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:54:38 +02:00
18cd879dbb Review 1.5: server and check pages: one overview query, paused checks are not run, bad numbers refused, seconds kept
- /servere reads the overview in one query and says which paused servers and checks are left out of the active
  findings; admin notes use InstanceSetup.WhoMayChangeText as main.
- The check form refuses a port, interval, timeout or limit that is not a number (Danish message) and clamps
  minutes before multiplying; an interval set to part of a minute through the API is shown and kept in seconds.
- "Kør nu" is refused for a paused check (message, no button); ?koersel= shows that run.
- A finding of a removed server or check does not link to a missing page and says why it is not active.
- The forecast text uses the history the forecasts were computed from; the grant script text matches the new
  script (-WhatIf first, -WinRM, -EnableFirewall -AgentAddress, -Remove only what it added).
- HTTP addresses are at most 2000 characters.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:53:26 +02:00
20829890e9 Review 1.5: API v1 redacts the agent's account, logins, user names and URL queries; history, event log metadata, paging
- The Sensitive helper (Value/Text) is shared (Api/Sensitive.cs) and gains ServerText ("Agentens konto (…)" and the
  test's account), Url (query left out), ServerTest and CheckResult.
- Windows servers and checks: CreatedBy/UpdatedBy, RequestedBy, LastError, area details, rule-state reasons, the
  check's SQL login and an HTTP address's query (settings.url and target) are "[udeladt]" unless
  ApiIncludesSensitiveEvidence is on. The grant script endpoint then returns the shipped template, Account
  "[udeladt]" and a note without the service account.
- GET /agent-requests/{id} rewrites ServerTest results (account and texts) and SyntheticCheckRun results (failure
  message, sensitive details) like ResultJson does for connection tests, and names serverId/checkId.
- Tests and runs reuse a request already waiting (said in the descriptions); running a paused check is 409.
- GET /server-findings: limit (max 500) and continuation with X-Continuation, evidence's sensitive flag, status,
  scopePaused/scopeRemoved; GET /server-findings/{id} has history. GET /windows-servers/{id}/events returns
  { capturedAt, method, ignored, coverage, events }. The server list/one server has latest, worstDisk, soonestFull
  and findings per severity; checks have findings per severity.
- Admin texts as main (AdminGroups, else the local Administrators); service lists trimmed and deduplicated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:53:26 +02:00
1c9d24a6a2 Review 1.5: repository: reuse waiting agent requests, active findings only on monitored servers and checks, cleanup on removal
- AddServerRequestAsync returns a ServerTest/SyntheticCheckRun already waiting for the agent (Pending, same kind and
  target) instead of queueing another one, like AddAgentRequestAsync; AgentRequestRow carries TargetId.
- Active server findings and the overview counts leave out removed servers and checks, and paused ones across all
  (a paused one's own list keeps its latest findings, as for instances); findings say ScopeRemoved/ScopePaused.
  GetServerFindingsAsync pages with OFFSET/FETCH.
- Removing a server (in the web UI or a SQL Server host by the agent's sync) detaches its synthetic checks in the same
  transaction (ServerId NULL, serviceName out of the settings); check queries only join servers in use.
- Removing a check clears its sealed password, key id, secret error and last message; a late agent result does not
  write to a removed check. (0018: UPDATE on LastMessage for the web login.)
- 0018: settings and service lists as nvarchar(max) (no 500 on long æøå queries); indexes on SampledAt/LastAt/RunAt
  for the retention deletes. Run details that do not fit are trimmed from the end instead of all dropped.
- One query for the overview's latest CPU/memory and disk collection per server.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:53:26 +02:00
53ffc008f0 Review 1.5: SQL Scripts describe the reviewed Windows server and check rules
Headers of SQLI-WIN-001/006/007/008/009/010/012/013 and SQLI-SYN-001 follow the rule
changes (not measurable instead of passed, caps and summary findings, hourly 24-hour
counts, per-user and updater tasks, neutral check titles); SQLI-WIN-010's severity
gains Low when the event log cannot be read (also in README.md).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:45:44 +02:00
7e66dc94c4 Review 1.5: server and check rules never pass unmeasured, cap and stabilize findings, keep sensitive text out of titles
- SQLI-WIN-006 is "ikke målbar" (with the forecaster's note) until a volume has enough
  history; titles "Drev X er fuldt (om ca. N dage | inden for et døgn)", as SQLI-WIN-005.
- SQLI-WIN-007/008: the startup grace period is "ikke målbar" instead of passed, so
  open findings are not resolved and reopened at every restart. One matcher for both
  rules (name or display name, prefix*). SQLI-WIN-007 is capped by
  Services:MaxFindings (default 15, 1-100) with one summary finding for the rest.
- SQLI-WIN-009: stable order (category, oldest first, key) and one summary finding
  over MaxFindings (now bounded 1-100).
- SQLI-WIN-010 only says "uden tegn på at det skete uventet" when the event log was
  read past the boot; before that "ikke målbar", and Low "årsagen kan ikke ses" when
  the log cannot be read.
- SQLI-WIN-012/013: Tasks:IgnoreUserAndUpdaterTasks (default true) leaves out per-user
  and updater tasks; TaskThresholds.Watches is the one filter for the agent and the
  rules. A SID is never in a title or subject, only in sensitive evidence.
- SQLI-SYN-001: neutral title "{navn} fejler ({n} gange i træk)", the failure message
  only in sensitive evidence; "mindst N" and "... eller tidligere" when every result
  given failed; "findes ikke" for a missing service; evidence "Tjenestens tilstand".
- SQLI-SYN-003: "udløber inden for et døgn" / "om 1 dag".
- Agent (minimal): the task filter is TaskThresholds.Watches, and performance samples
  get the configured interval.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:45:44 +02:00
2345b199fa Review 1.5: disk forecasts, 24-hour event counts, performance gaps and per-user tasks in Core
- DiskForecast.EnoughHistory: false for "ingen målinger endnu" and "for lidt historik",
  so a rule can say "ikke målbar" instead of "fyldes ikke op". Days are counted from
  now (a last point older than its hour is subtracted); FullAt takes the time the
  forecast was made; Describe says "fuld nu" at 0 days.
- ServerEventGroup.Buckets: counts per clock hour, and Recent sums only the hours with
  events in the last 24 hours (FirstAt within the window), so a recurring error no
  longer adds up from when it was first seen. Older groups without buckets still merge.
- ServerPerformanceMath.Sample takes the interval: after a gap of more than three
  intervals CPU and pages/s are null rather than the gap's average stamped now.
  Commit percent is computed in floating point.
- TaskResults.IsUserOrUpdaterTask (a user's SID in the name, OneDrive/Edge/Google
  updaters, User_Feed_Synchronization), HasUserSid and Display (SID replaced by a
  short hash for titles and subjects).
- Tests: forecast from real history, irregular sampling, identical timestamps,
  a shrunk volume, days from now; an error every ten hours over three days; a
  sample after a gap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:45:44 +02:00
f27a060e4f Review 1.5: docs: what Windows servers and synthetic checks store, the stricter SQL check, no hub or PDF yet
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:33:04 +02:00
697d243658 Review 1.5: grant-windows-server.ps1 backs up, removes exactly what it gave, and never more than read
The grant script changes SCM and WMI security on customer servers. Before, a re-run
replaced every ACE of the account on a WMI namespace (also a customer's Deny), -Remove
took the account out of groups and ACEs it had before, only the SCM SDDL was backed up
(and never used), Remote Management Users also opened PowerShell Remoting, and
-EnableFirewall narrowed Windows' built-in rules for every other tool and was never undone.

- Checks everything before changing anything: elevation, the account resolves (broad
  groups refused), Deny ACEs stop the script, -EnableFirewall needs -AgentAddress.
- Backs up the original SDDLs (SCM, WMI namespaces, WinRM), group memberships and
  firewall rules before the first change; records what it added in a state file in an
  administrators-only folder; -Remove removes only that.
- SDDL handled with RawSecurityDescriptor (GetSD/SetSD, sc sdshow/sdset): exactly one
  ACE added in canonical order, verified before writing and after; aliases (NS) work.
- No Remote Management Users: -WinRM adds GENERIC_READ (no Execute) to WinRM's RootSDDL;
  DCOM is the default.
- Firewall: its own "SQLInsight Agent: ..." rules only from the agent's address,
  removed again with -Remove; Windows' rules untouched.
- -WhatIf, groups by SID, Danish messages, PowerShell 2.0 syntax.
- ServerGrantScript.For does not fill in an account that could end the PowerShell
  string (typographic quotes, control characters); tests run the SDDL logic in
  Windows PowerShell against real default SDDLs.
- docs/windows-servere.md: rights, safety net, firewall.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:31:49 +02:00
cb99778a3f Review 1.5: SQL Scripts for Windows servers and synthetic checks follow main's structure and registry
After the rebase onto 1.4 the 16 SQLI-WIN/SQLI-SYN scripts had their own /* */ format,
so SqlScriptTests (rule ids, review header, -- Kilde: blocks) failed.

- The scripts have main's header (Regel-id, Titel, Alvor, Versioner, Vurdering,
  Grænseværdier, Rettigheder, Data kommer fra, Regelkode, Read-only); the Windows
  WQL/XPath stays word for word in comments.
- The synthetic SQL check's SQL is registered (SyntheticCheckRunner.UseDatabaseSql,
  BeginTransactionSql, RollbackSql, SyntheticCheckSettings.DefaultQuery) and shown
  as -- Kilde: blocks in SQLI-SYN-001/002 and _collectors/SyntheticCheckRunner.sql.
- SqlScriptTests know ServerRuleCatalog; only those rules may be without SQL blocks.
- ServerScriptTests: every CimQueries query/filter is in a SQLI-WIN script word for word.
- The SQL check cancels the rest of a large answer instead of draining it on close.
- Rule counts: README (108 built-in: 92 SQL Server + 13 Windows + 3 synthetic),
  SQL Scripts/README (rule table, 124 rules, Windows/synthetic section), RuleCatalogTests,
  CONTRIBUTING (how to add a server rule).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:18:24 +02:00
8d092b6a35 Review 1.5: the read-only check of typed queries can no longer be bypassed
The synthetic SQL check (and custom rules) used a regex that ended an [identifier]
at the first ] and a block comment at the first */, so "[x]]'] DELETE ..." hid a
DELETE inside what it took for a string. T-SQL also needs no semicolon between
statements, so "SELECT 1 COMMIT" or "SELECT 1 WRITETEXT ..." passed.

- ReadOnlyQuery lexes T-SQL as SQL Server does (nested comments, '' ]] ""),
  refuses unterminated comments/strings/identifiers, every statement keyword
  (COMMIT, ROLLBACK, SET, DECLARE, WRITETEXT, UPDATETEXT, RAISERROR, CHECKPOINT,
  ENABLE/DISABLE, RECEIVE/SEND ...), NEXT VALUE FOR (sequences are not rolled
  back) and update/exclusive lock hints, and a second SELECT unless joined by
  UNION/EXCEPT/INTERSECT.
- CustomRuleDefinition.QueryProblems uses it, so the synthetic check validates the
  same way when saved and before it runs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:08:34 +02:00
3edc69b48f Servers tests: give the local CIM test room when the whole suite runs at once
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:59:22 +02:00
4c2d5252ad Docs: Windows-servere og syntetiske tjek (1.5)
- docs/windows-servere.md: what is collected and how often, how the agent reads
  servers (WS-Man, DCOM fallback, event log over MS-EVEN6), the exact least-privilege
  rights and grant-windows-server.ps1 (never local administrator), known limits,
  "ikke målbar" with the exact reason, firewall ports, the disk forecast, event
  noise filtering, rules with their thresholds, retention, configuration and API.
- docs/syntetiske-tjek.md: the kinds, the AX logon (port, then .NET Business
  Connector in a separate process, or clearly only the port), service running but
  not answering, sealed SQL passwords, rules, firewall, configuration and API.
- Linked from installation, integration (API table) and databehandling (retention,
  nothing sent to the hub); on the website in the documentation index, and the rule
  list shows the Windows server and synthetic check rules in their own areas.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:59:22 +02:00
063243532d Web: style the field groups of the synthetic check form
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:59:03 +02:00
1c18bcfab8 Web: Windows-servere and Syntetiske tjek pages, and the same in API v1 (scope config:write)
- Menu group "Servere" with Windows-servere (/servere) and Syntetiske tjek
  (/syntetiske-tjek).
- /servere: every server with status (overvåges, delvist målbar, kan ikke læses),
  CPU, free memory, the fullest disk and the nearest "fuld om ca. N dage", findings;
  add application/AOS/file servers. /servere/{id}: findings and what is not
  measurable and why, CPU/memory for the last day, disks with forecasts, watched
  and stopped services, grouped event log errors, scheduled tasks, rights per area
  with the latest test, the least-privilege PowerShell script (copy or download,
  UTF-8 with BOM), role and service lists, pause and remove. /servere/fund/{id}.
- /syntetiske-tjek: checks with status and response time; one form for every kind
  (TCP, HTTP(S), SQL login + query, DNS, AX logon) with an optional server and
  service; a SQL login password is sealed for the agent at once and never shown.
  /syntetiske-tjek/{id}: latest result (incl. "kun porten er testet"), findings,
  response times, history, edit, run now, pause, remove.
- API v1: /windows-servers (+ test, grant-script, performance, disks, services,
  events, tasks, rule-states), /server-findings, /synthetic-checks (+ run, results,
  rule-states); all in OpenAPI. Passwords are write-only; event samples, failure
  messages and sensitive evidence are "[udeladt]" unless
  SQLInsight:Web:ApiIncludesSensitiveEvidence is on. /api/v1/rules lists the new rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:58:52 +02:00
842286d57d SQL Scripts for the Windows server and synthetic check rules (SQLI-WIN-001..013, SQLI-SYN-001..003)
One reviewable file per rule id with a Danish header: what and why, thresholds
with config keys and defaults, severity, required rights (never local
administrator), supported versions, collector file and member, and the
evaluation in plain Danish. The Windows rules use no SQL: their exact CIM/WQL
queries, event log XPath and a read-only PowerShell snippet to verify them are in
a comment block. SQLI-SYN-001/002 carry the exact SQL a SQL check runs (session
settings, transaction that is always rolled back, SELECT 1). A test checks every
server and check rule has its file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:58:36 +02:00
6eae719e95 Agent: monitor Windows servers and run synthetic checks; server tests and check runs on request
- ServerMonitoringService keeps the SQL Server hosts in step with the monitored
  instances (SQLInsight:Servers:IncludeSqlHosts), picks up servers added, paused or
  changed in SQLInsight Web within a minute, and runs each server's due areas with
  a limit on parallelism. Default intervals: 1 min CPU/memory, 15 min disks and
  tasks, 5 min services and events (SQLInsight:Servers:Intervals).
- ServerMonitor: one CIM session per cycle; per-minute samples, disk samples and
  the "fuld om ca. N dage" forecast from the stored history, grouped events (read
  from where the last read stopped), services and tasks. A missing right makes
  only that area "ikke målbar" with the exact reason, logged once per change; a
  server that does not answer is notified once, and again when it answers.
  Survives an agent restart (samples, boot time, event position).
- SyntheticCheckService runs checks on their interval, opens a SQL login password
  sealed for the agent (AgentCredentialKey.OpenContext), looks up the linked
  Windows service when a check fails, stores results and findings, notifies with
  links to /servere/fund/{id}.
- AgentRequestService hands ServerTest and SyntheticCheckRun to ServerRequestHandler.
- appsettings.json: SQLInsight:Servers and SQLInsight:SyntheticChecks with every
  threshold, interval and retention.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:58:36 +02:00
7082b278e3 Collection: Windows servers over CIM (WS-Man, DCOM fallback) and synthetic check runners
New project SQLInsight.Servers:
- CimWindowsServerSource (Microsoft.Management.Infrastructure, the API behind
  Get-CimInstance): WS-Man first, DCOM if that fails, a local session for the
  agent's own computer, as the agent's identity. Only SELECT queries (CimQueries)
  and Task Scheduler's read-only GetInfo. CPU and paging from raw counters, disks
  (Win32_Volume, Win32_LogicalDisk fallback), services, scheduled tasks, and the
  System/Application logs through the Event Log service (MS-EVEN6, XPath filter on
  the server) with Win32_NTLogEvent as fallback.
- ServerErrors: access denied, missing namespace, RPC/WinRM unreachable and
  timeouts become "ikke målbar" reasons that name the missing right and the grant
  script; never local administrator.
- WindowsServerTester: tries every area once for the web UI's test.
- SyntheticCheckRunner: TCP, HTTP(S) with expected status/text and TLS certificate
  expiry (also for refused certificates), SQL login + read-only query (re-validated,
  in a transaction that is always rolled back), DNS, and the Dynamics AX logon:
  AOS port, then a logon through the .NET Business Connector in a separate Windows
  PowerShell process with a timeout when it is installed; otherwise the result
  says clearly that only the port was tested.

Tests: error mapping, queries, AX command/parsing, runners against local TCP and
HTTP(S) servers with a self-signed certificate, the CIM source against this
computer, and the SQL check against the test instance.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:58:18 +02:00
25ecdbc3e7 Repository: Windows servers, samples, events, synthetic checks and their findings (0018)
Migration 0018_windows_servers_synthetic_checks:
- insight.WindowsServers (SQL Server hosts kept in step by the agent, servers added
  in SQLInsight Web), ServerPerformance (per minute, one row per sample),
  ServerDiskSamples (forecast history), ServerEvents (grouped like ErrorLogEvents),
  ServerSnapshots (latest per area).
- insight.SyntheticChecks (SQL login password sealed for the agent, as in 1.3) and
  SyntheticCheckResults.
- insight.ServerFindings and ServerRuleStates, scoped to a server or a check.
- AgentRequests.TargetId and the kinds ServerTest and SyntheticCheckRun.
- SQLInsight Web gets INSERT and column UPDATE only; host, name and kind are fixed,
  nothing the agent records can be written, no DELETE.

Findings are opened, updated and resolved like instance findings; a round where a
rule is not measurable keeps what was found open. Retention for samples, disk
history, events, check results and resolved findings (RunServerMaintenanceAsync).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:58:18 +02:00
ba7c751b5c Rules: Windows servers (SQLI-WIN-001..013) and synthetic checks (SQLI-SYN-001..003)
Scoped rules with their own context (a Windows server, a synthetic check) that
produce the same findings and outcomes as the instance rules; missing data is
"ikke målbar" with the collector's reason. Thresholds are settings
(SQLInsight:Servers, SQLInsight:SyntheticChecks).

- Sustained CPU, low memory (share and MB), paging with low memory, commit charge.
- Free disk space and "fuld om ca. N dage" forecasts.
- Stopped automatic services (minus those that stop by design, and a grace
  period after a restart) and watched services that must run.
- Windows event log groups: important kinds at once, ordinary errors when they
  repeat, capped per server; sample messages are sensitive.
- Unexpected restart (Kernel-Power 41, EventLog 6008, BugCheck), optional uptime.
- Scheduled tasks that failed or did not run as planned.
- Synthetic checks failing (twice in a row), "svarer ikke, selv om tjenesten
  kører", slow answers, TLS certificates about to expire.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:58:18 +02:00
ae20954a91 Windows servers and synthetic checks core: data, disk forecast, event dedup, grant script
- Core/Servers: what the agent collects from a Windows server (CPU and memory from
  raw counters, disks, services, events, scheduled tasks), roles and sources, host
  validation and the computer behind a SQL Server data source.
- DiskForecaster: "fuld om ca. N dage" from a least-squares line over 14 days of
  SQLInsight's own samples, only since the volume last changed size, with sanity
  bounds (history, samples, R², max one year).
- ServerEventAnalysis: noise filter, grouping and a 24-hour window for System and
  Application events, like the SQL Server error log in 1.2.
- db/permissions/grant-windows-server.ps1: least-privilege rights for the agent's
  account (groups, WMI namespaces, SCM read), never local administrator; filled in
  by ServerGrantScript.
- Core/SyntheticChecks: kinds, settings, validation (read-only SQL), results.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:58:17 +02:00
90d37be8a5 Version 1.4.0
Some checks failed
deploy community / deploy (push) Successful in 18s
deploy hub / deploy (push) Successful in 19s
CI / Build og unit tests (push) Successful in 8s
CI / MSI-installer (push) Successful in 15s
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Has been cancelled
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Has been cancelled
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Has been cancelled
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:45:31 +02:00
31fa89b385 Review 1.4: AX detection survives passing errors; revoke only what the grant gave; smaller fixes
- A lock timeout or deadlock while detecting (e.g. during an AX table
  synchronisation) dropped the database from AX monitoring for an hour, so
  its findings resolved and reopened. The earlier detection is kept and
  retried within 5 minutes. A cancelled collection is no longer reported as
  "læsningen fejlede".
- A lock timeout kept the earlier batch picture forever; now at most 15
  minutes (MeasuredAt), so a job that has started is not reported late from
  an old picture.
- revoke-dynamics-ax-read.sql revoked SELECT on same-named tables in every
  schema, also a DENY set by a DBA. It now finds the AX schema like the grant
  script and revokes only granted SELECT there.
- SQLI-AX-006 skips blocked sequences (as -005/-008 do); forecasts under a
  day say "inden for et døgn" instead of "om ca. 0 dage".
- Integration tests: the SQL Scripts run as-is in main's format, and the
  shared connection has the agent's session settings after the AX reads.
- docs/dynamics-ax.md: history bounds, lock-timeout window, session settings,
  /detection-problems, API redaction.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:45:31 +02:00
13f22c7541 Review 1.4: AX batch rules: one finding per job, stuck jobs only once, no crash without a start
- Axapta 3.0/4.0 keep a BATCH row per recurrence, so a failed run and the
  next waiting run share a job key and gave two SQLI-AX-001 findings with the
  same subject (the repository kept the last, the hub the first). The batch
  rules now keep one finding per subject, the most severe.
- SQLI-AX-004 treated a single earlier 10-hour run as "normally that long"
  and stayed silent; it now needs BatchHistoryMinimumRuns runs, like
  SQLI-AX-002. A stuck job is SQLI-AX-004's finding only, not also -002's.
- Running(now)!.Value threw for an executing job with neither a start time
  nor a first-seen time (and a negative duration meant clock skew); such jobs
  are skipped.
- SQLI-AX-010 no longer calls every session in an AX database "from Dynamics
  AX", and shows the raw CONTEXT_INFO as sensitive evidence.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:29:35 +02:00
8365ba5357 Review 1.4: AX pages and API leave out removed instances; bounded, complete reads
- DynamicsAxView (pages and API v1) listed instances removed from monitoring;
  /batch-runs without ?instance= returned runs of every instance. Both now
  only cover the instances the overview shows.
- AX blocking read the instance's newest 200 blocking events and filtered
  for AX afterwards, so other blocking could push AX blocking out entirely.
  GetBlockingInDatabasesAsync filters on the AX databases in SQL (OPENJSON)
  with its own TOP (1000).
- /sequence-samples loaded every sample and cut at 10,000 in memory; the limit
  is in SQL now, with X-Truncated when there are more.
- Two new runs with the same run key in one snapshot would break the
  primary key of insight.AxBatchRuns; they are deduplicated first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:29:34 +02:00
f4d2603181 Review 1.4: AX texts in API v1 and the hub without logins or host names; API has what the pages show
- "Ikke målbar" texts, notes and detection problems carry the GRANT text with
  the agent's login and database user. API v1 (batch-jobs, number-sequences,
  recids, databases, rule-states) returned them as they were, although logins
  are redacted everywhere else. AxReadAccess.WithoutPrincipals replaces them
  with the placeholder unless sensitive values are allowed; the grant script
  also hides a database user that differs from the login.
- SQLI-AX-010 put the host and program name (in 2-tier AX 3.0 the user's own
  PC) in the finding's explanation, which reaches the hub unredacted for an
  opted-in database. They are only in the sensitive evidence now, with the raw
  CONTEXT_INFO; the CONTEXT_INFO parsing says it is not verified against
  customer data.
- A database named e.g. "AX · test" was read from a subject as "AX", whose
  hub opt-in then applied. Middle dots in the database are doubled in AX
  subjects, and the AX community rules' subjects are built the same way.
- API v1 now also has the detection problems (/detection-problems), what each
  raw batch STATUS means, and the thresholds behind the number sequence and
  RecId pages; the blocking explanation is no longer redacted (it holds
  nothing sensitive now).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:29:34 +02:00
66ed4e11b3 Review 1.4: AX 3.0/4.0 local times in the AX time zone, with its DST rules
Axapta 3.0 and AX 4.0 store batch and cleanup times as a local date plus
seconds. They were turned into UTC with the SQL Server's offset at collection
time, so every time from the other side of a DST change was an hour off (a job
planned 01:50 CET on the spring night looked 70 minutes late at 03:00 CEST),
and a SQL Server running UTC behind a CET AOS shifted every time.

AxLocalTime converts with the rules of the AX time zone on that date:
SQLInsight:DynamicsAx:LocalTimeZone (Windows or IANA id), else the agent's own
zone when its offset matches the SQL Server's, else the server's offset (to the
nearest quarter hour) with a Danish data note. The skipped spring hour and the
repeated autumn hour get standard time (deterministic, never throws, never a
false "late"); seconds outside 0..86400 are unknown and 86400 is midnight. The
AX 3.0 cleanup time is local by version, also when LATESTCLEANTIME is missing.
AX 2009+ UTC times are untouched. Also: remaining numbers cannot overflow for
HIGHEST = Int64.MaxValue, and run keys use the invariant culture.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:19:17 +02:00
fdaa7e76bf Review 1.4: the AX batch history watermark no longer gets stuck
After the first read, BATCHJOBHISTORY was read from the watermark minus an
overlap of 2000 RecIds with TOP (1000), oldest first. From AX 4.0 RecIds are
handed out per table and are dense, so once 1000 rows existed in the overlap
every read returned the same old rows: the watermark never moved and new runs
were never read again (every AX 2009/2012 installation with more than ~1000
history rows). The read after the watermark now takes the overlap plus 1000
rows (TOP (3000)), so at least 1000 new runs are read each time. Test with
20,000 dense RecIds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:15:49 +02:00
be039a6f73 Review 1.4: AX reads put the agent's session settings back
AxSql.Reset ran SET LOCK_TIMEOUT -1 and READ COMMITTED after every AX read
(and after the AX detection in discovery). The collectors of an instance
share one connection, so every collector after the AX ones waited on locks
without limit instead of the agent's 5 s, and no longer read uncommitted.
Reset now restores the agent's own settings, from one place
(Core.Instances.AgentSession) that SqlConnectionFactory.SessionSettings also
uses. The AX reads themselves are unchanged: READ COMMITTED (or SNAPSHOT)
with a 2 s lock timeout inside sp_executesql, never NOLOCK.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:14:24 +02:00
3a0bbe22f1 Review 1.4: SQL Scripts for Dynamics AX in the 1.3 structure
The AX scripts written in 1.4 (own header style and a generator) now follow
main's "SQL Scripts" structure: the review header, "-- Kilde:" blocks
registered in SqlScriptSources and checked word for word against the code.

- SqlScriptSources: the shared AX detection (DatabasesSql, LoginSql,
  BuildDetectionSql), companies, batch jobs, batch history (first read and
  from the watermark) and number sequences / RecId for Axapta 3.0, AX 4.0,
  AX 2009 and AX 2012; BlockingCollector split 2008 / 2005, as CONTEXT_INFO
  comes from sys.dm_exec_sessions from 2008 and from sysprocesses on 2005.
- SQLI-AX-001..010 and _collectors/Ax*.sql in main's format; COM-AX-001..007
  with the 1.4 queries, the AX detection and USE [AXDB]; BLK-001,
  BlockingCollector and DatabaseDiscovery updated (CONTEXT_INFO,
  sp_executesql).
- Scripts_are_read_only stays strict (no INTO #temp anywhere) and now also
  checks the batch inside EXEC [db].sys.sp_executesql N'...'.
- SQLINSIGHT_UPDATE_SQL_SCRIPTS=1 (from 1.4) rewrites out-of-date blocks from
  the code; the 1.4 generator test now checks that every AX script shows its
  sources for every AX version and says what is unverified.
- README: SQLI-AX-001..010, the AX collector scripts, 108 rules.
- AxDatabaseDetector: LoginSql and BuildDetectionSql as members, so the
  registry sees them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:11:45 +02:00
08cd741b52 Dynamics AX: notice a GRANT within minutes, one AX lookup per custom-rule run
The detector looks again every 5 minutes at an AX database where the agent
lacks part of the read access, so a GRANT takes effect quickly. The AX SQL
test checks that only standard AX tables are read. The hub end-to-end test
expects no AX rule outcomes without the opt-in. Docs: what 1.5's synthetic
AX checks can reuse.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 13:54:36 +02:00
cd72f14883 Docs: Dynamics AX (1.4)
docs/dynamics-ax.md: detection, the optional read access per version, how
AX is read (SELECT only, TOP, 2 s lock timeout or snapshot, never NOLOCK),
the rules SQLI-AX-001..010 with thresholds, batch times and the unverified
AX 3.0 STATUS mapping, forecasts from SQLInsight's own samples, AX users
behind blocking (CONTEXT_INFO, honest for 3.0), the hub opt-in, the API and
how to drop in a real 2009/2012 schema. AX 2009/2012/F&O are marked as based
on Microsoft's documented schema, not verified against customer data.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 13:54:36 +02:00
d574057114 Integration tests: fake Axapta 3.0 (bmssa) and AX 2012 R2 databases
Built from the schema files in the SQL Server 2022 container and read as the
least-privilege agent login: detection and discovery, 'ikke målbar' with the
exact GRANT and measuring after it (no customer table readable, no data
changed), the shipped grant/revoke scripts, AX 2012 history by RECID read
once, a locked AX table skipped within seconds, the SQL Scripts run as-is,
repository retention and read-back, and the web pages and API v1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 13:54:23 +02:00
e2284657ae Web and API v1: Dynamics AX pages and endpoints
Dynamics AX in the menu (shown when AX databases are recognised): AX-databaser
(version, schema, companies, read access and the exact GRANT per database),
Batchjob, Nummerserier, RecId and AX-brugere bag blokering. The same data in
API v1 under /api/v1/dynamics-ax/ (databases, grant-script, batch-jobs,
batch-runs, number-sequences, recids, sequence-samples, blocking), documented
in OpenAPI, with AX users, hosts, statements and the agent's login redacted
unless ApiIncludesSensitiveEvidence is on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 13:54:23 +02:00
3e1cc74f0e Dynamics AX rules SQLI-AX-001..010 and their SQL Scripts
Batch jobs: failed (001), longer than their own history (002), late or never
started (003), stuck in Executing (004). Number sequences: running out with a
forecast from SQLInsight's own samples (005), format shorter than HIGHEST
(006), blocked but still referenced (007), continuous and not cleaned up
(008). RecId running out, flagged far earlier for AX 3.0's 32-bit RecId
(009). AX user behind blocking from CONTEXT_INFO, honest where AX 3.0 cannot
tell (010). Subjects carry the database, so findings follow the hub opt-in.

SQL Scripts/<rule>.sql (and _collectors/) for every AX rule added or changed
in 1.4, incl. COM-AX-001..007: a Danish header plus the exact SQL SQLInsight
sends, generated from the code and checked by SqlScriptTests (rewrite with
SQLINSIGHT_UPDATE_SQL_SCRIPTS=1).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 13:54:23 +02:00
36ad4c0656 Repository and agent: Dynamics AX history (migration 0017), seeding, hub opt-in
0017_dynamics_ax adds insight.AxBatchRuns (completed batch runs, EventDays
retention) and insight.AxSequenceSamples (one NEXTREC/NEXTVAL sample per
sequence per day, HourlyDays retention). The agent reads them back after a
restart so job comparisons and forecasts continue, takes its thresholds from
SQLInsight:DynamicsAx, and only sends findings of the built-in SQLI-AX rules
to the hub for databases with the AX opt-in ticked; AX rule reasons (which
can name tables, logins and GRANT text) are replaced by a fixed text.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 13:53:34 +02:00
6b945295d0 Dynamics AX collectors: shared detector, batch jobs, number sequences and RecId
dynamics-ax (hourly) lists the monitored AX databases with version, schema,
companies and read access; discovery and the AX community rules (now marked
dynamicsAx) use the same detection. dynamics-ax-batch (5 min) reads BATCH
(3.0/4.0) or BATCHJOB/BATCHJOBHISTORY (2009+), keeps each job's own run
history; dynamics-ax-sequences (hourly) reads NUMBERSEQUENCETABLE and
SYSTEMSEQUENCES and forecasts from SQLInsight's own daily samples. All reads
run through [db].sys.sp_executesql under read committed with a 2 s lock
timeout (snapshot when allowed), bounded with TOP, never NOLOCK, never
written. Blocking now captures CONTEXT_INFO for the AX user mapping.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 13:53:34 +02:00
4ebae4efe6 Dynamics AX core: central detector, per-version schema adapters, read-access GRANT
One place recognises AX (version 3.0/4.0/2009/2012/2012 R2/F&O, schema owner,
model store) from metadata only. Per-version adapters hold all table/column
knowledge, check every column in sys.columns before use and degrade to
'ikke målbar' with the exact column or the exact GRANT text. Only AX 3.0 is
verified against a real customer schema; 2009/2012 are written from
Microsoft's documented schema (docs/dynamics-ax/*.documented.tsv, replaced
by a real export when one is dropped in). grant-dynamics-ax-read.sql is
version- and schema-aware (bmssa) and covers exactly the adapters' tables.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 13:52:58 +02:00
66aa47ecb3 Deadlocks: read system_health files without a temp table
The 2012+ query used SELECT ... INTO #system_health, a write to tempdb.
One SELECT with ROW_NUMBER() now returns the deadlock rows and the newest
row (the read position) in the same pass. SQL Scripts and the read-only
test no longer allow any exception.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 13:47:31 +02:00
208 changed files with 24944 additions and 302 deletions

View file

@ -40,6 +40,8 @@ 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.3.0</Version>
<Version>1.5.2</Version>
</PropertyGroup>
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('Tests'))">

View file

@ -22,6 +22,7 @@
<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, 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).
> **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).
---

View file

@ -5,6 +5,9 @@
-- 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,
@ -14,8 +17,10 @@
--
-- 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). 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
-- 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
-- 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).
--
@ -34,15 +39,20 @@
-- 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. Intet ændrer data,
-- indstillinger eller objekter.
-- 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.
-- 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
-- database, du vil se.
-- Forespørgslerne i databasen er vist for AXDB: ret [AXDB] til din Dynamics AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
@ -65,10 +75,42 @@ 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 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
-- 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
GO

View file

@ -5,6 +5,9 @@
-- 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
@ -15,8 +18,10 @@
--
-- 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). 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
-- 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
-- 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).
--
@ -35,15 +40,20 @@
-- 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. Intet ændrer data,
-- indstillinger eller objekter.
-- 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.
-- 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
-- database, du vil se.
-- Forespørgslerne i databasen er vist for AXDB: ret [AXDB] til din Dynamics AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
@ -66,10 +76,42 @@ 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 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
-- 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
GO

View file

@ -5,6 +5,9 @@
-- 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å,
@ -14,8 +17,10 @@
--
-- 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). 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
-- 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
-- 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).
--
@ -34,15 +39,20 @@
-- 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. Intet ændrer data,
-- indstillinger eller objekter.
-- 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.
-- 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
-- database, du vil se.
-- Forespørgslerne i databasen er vist for AXDB: ret [AXDB] til din Dynamics AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
@ -65,10 +75,42 @@ 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 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')
-- 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
GO

View file

@ -5,6 +5,9 @@
-- 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
@ -14,8 +17,10 @@
--
-- 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). 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
-- 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
-- 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).
--
@ -34,15 +39,20 @@
-- 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. Intet ændrer data,
-- indstillinger eller objekter.
-- 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.
-- 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
-- database, du vil se.
-- Forespørgslerne i databasen er vist for AXDB: ret [AXDB] til din Dynamics AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
@ -65,10 +75,42 @@ 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 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')
-- 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
GO

View file

@ -5,6 +5,9 @@
-- 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
@ -14,8 +17,10 @@
--
-- 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). 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
-- 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
-- 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).
--
@ -34,15 +39,20 @@
-- 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. Intet ændrer data,
-- indstillinger eller objekter.
-- 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.
-- 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
-- database, du vil se.
-- Forespørgslerne i databasen er vist for AXDB: ret [AXDB] til din Dynamics AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
@ -65,10 +75,42 @@ 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 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
-- 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
GO

View file

@ -5,6 +5,9 @@
-- 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
@ -14,8 +17,10 @@
--
-- 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). 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
-- 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
-- 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).
--
@ -34,15 +39,20 @@
-- 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. Intet ændrer data,
-- indstillinger eller objekter.
-- 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.
-- 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
-- database, du vil se.
-- Forespørgslerne i databasen er vist for AXDB: ret [AXDB] til din Dynamics AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
@ -65,10 +75,42 @@ 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 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
-- 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
GO

View file

@ -5,6 +5,9 @@
-- 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
@ -15,8 +18,10 @@
--
-- 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). 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
-- 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
-- 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).
--
@ -35,15 +40,20 @@
-- 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. Intet ændrer data,
-- indstillinger eller objekter.
-- 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.
-- 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
-- database, du vil se.
-- Forespørgslerne i databasen er vist for AXDB: ret [AXDB] til din Dynamics AX-database.
-- ===================================================================================================================
-- -------------------------------------------------------------------------------------------------------------------
@ -66,10 +76,42 @@ 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 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')
-- 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)
GO

View file

@ -7,6 +7,10 @@ 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 |
@ -14,7 +18,9 @@ identisk med koden (se [Hold mappen i sync](#hold-mappen-i-sync)).
| `<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
@ -61,10 +67,9 @@ 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`.
**É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.
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.
- **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.
@ -75,26 +80,48 @@ 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 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, fra 1.5,
`ServerRuleCatalog`) 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.
anderledes. Kun Windows-server- og syntetiske-tjek-reglerne må være uden SQL-blokke.
- `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_apart_from_the_documented_temp_table`: ingen skrivende sætninger ud over undtagelsen ovenfor.
- `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.
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
98 regler: 82 indbyggede (C#) og 16 community-regler (JSON).
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).
| Regel-id | Fil | Titel | Collector(s) | Alvor |
|---|---|---|---|---|
@ -102,6 +129,16 @@ 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 |
@ -166,6 +203,9 @@ 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 |
@ -180,6 +220,19 @@ 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 |
@ -220,7 +273,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) | 1 regel |
| [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 |
| [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 |
@ -229,3 +282,7 @@ 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) |

159
SQL Scripts/SQLI-AX-001.sql Normal file
View file

@ -0,0 +1,159 @@
-- ===================================================================================================================
-- 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

163
SQL Scripts/SQLI-AX-002.sql Normal file
View file

@ -0,0 +1,163 @@
-- ===================================================================================================================
-- 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

164
SQL Scripts/SQLI-AX-003.sql Normal file
View file

@ -0,0 +1,164 @@
-- ===================================================================================================================
-- 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

163
SQL Scripts/SQLI-AX-004.sql Normal file
View file

@ -0,0 +1,163 @@
-- ===================================================================================================================
-- 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

131
SQL Scripts/SQLI-AX-005.sql Normal file
View file

@ -0,0 +1,131 @@
-- ===================================================================================================================
-- 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

124
SQL Scripts/SQLI-AX-006.sql Normal file
View file

@ -0,0 +1,124 @@
-- ===================================================================================================================
-- 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

125
SQL Scripts/SQLI-AX-007.sql Normal file
View file

@ -0,0 +1,125 @@
-- ===================================================================================================================
-- 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

127
SQL Scripts/SQLI-AX-008.sql Normal file
View file

@ -0,0 +1,127 @@
-- ===================================================================================================================
-- 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

130
SQL Scripts/SQLI-AX-009.sql Normal file
View file

@ -0,0 +1,130 @@
-- ===================================================================================================================
-- 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

168
SQL Scripts/SQLI-AX-010.sql Normal file
View file

@ -0,0 +1,168 @@
-- ===================================================================================================================
-- 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.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
@ -71,7 +71,26 @@ 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 – 2008 R2
-- 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,
@ -81,7 +100,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.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
@ -95,7 +114,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.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)

View file

@ -31,11 +31,8 @@
-- Delt script: SQL Scripts/_collectors/DeadlockCollector.sql
-- Regelkode: src/SQLInsight.Rules/Concurrency/ConcurrencyRules.cs : DeadlockRule.Evaluate
--
-- 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.
-- 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.
-- 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
@ -59,14 +56,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 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;
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;
GO
-- Variant: SQL Server 2008 – 2008 R2, og når event-filerne ikke kan læses

View file

@ -0,0 +1,91 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,73 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,38 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,50 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,48 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,46 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,41 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,49 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,58 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,54 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,43 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,59 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,45 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,40 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,55 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,47 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,98 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,89 @@
-- ===================================================================================================================
-- 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

@ -0,0 +1,105 @@
-- ===================================================================================================================
-- 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,7 +1,8 @@
-- ===================================================================================================================
-- Collector: BlockingCollector ("blocking")
-- ===================================================================================================================
-- Hvad: Blokerede sessioner og deres blokerere lige nu, med sætning og åbne transaktioner.
-- 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).
-- Fil: src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs
-- Interval: 1 min som standard (SQLInsight:CollectorIntervals:blocking)
-- Versioner: Alle (SQL Server 2000 og nyere)
@ -12,7 +13,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
-- Bruges af: SQLI-BLK-001, SQLI-AX-010
-- 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.
@ -28,7 +29,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.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
@ -37,7 +38,26 @@ 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 – 2008 R2
-- 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,
@ -47,7 +67,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.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
@ -61,7 +81,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.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)

View file

@ -19,8 +19,10 @@
--
-- 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. Sessionen skifter aldrig database, og intet ændrer
-- data, indstillinger eller objekter.
-- 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).
-- 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.
-- ===================================================================================================================
@ -54,18 +56,25 @@ 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)
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')
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;
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 COUNT(DISTINCT o.name), MAX(CASE WHEN o.name = N'SQLDICTIONARY' THEN c.xprec END)
SELECT u.name, UPPER(o.name), UPPER(c.name), TYPE_NAME(c.xtype), c.xprec, c.length, 0, NULL
FROM [msdb].dbo.sysobjects o
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')
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
GO

View file

@ -12,11 +12,8 @@
-- GRANT VIEW SERVER SECURITY STATE TO [agent]; -- SQL Server 2022+
--
-- Bruges af: SQLI-DLK-001
-- 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.
-- 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.
-- 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.
-- ===================================================================================================================
@ -24,14 +21,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 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;
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;
GO
-- Variant: SQL Server 2008 – 2008 R2, og når event-filerne ikke kan læses

View file

@ -0,0 +1,50 @@
-- ===================================================================================================================
-- 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,6 +9,7 @@
<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" />
@ -25,6 +26,7 @@
<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

@ -0,0 +1,101 @@
/*
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

@ -0,0 +1,467 @@
<#
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

@ -0,0 +1,66 @@
/*
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): 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)).
**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)).
**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`.
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.
## Hvor ligger data?
@ -104,3 +104,17 @@ 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.

126
docs/dynamics-ax.md Normal file
View file

@ -0,0 +1,126 @@
# 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

@ -0,0 +1,129 @@
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

@ -0,0 +1,135 @@
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

@ -0,0 +1,53 @@
-- ===================================================================================================================
-- 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,6 +90,10 @@ 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
@ -184,4 +188,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)), **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.
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.

View file

@ -89,10 +89,13 @@ 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. 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, 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.
**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,6 +13,32 @@ 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).
@ -138,18 +164,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.3.0
installer/build-msi.sh 1.5.2
# 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.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
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
# 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.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 \
--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 \
--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 (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.
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.
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.

96
docs/syntetiske-tjek.md Normal file
View file

@ -0,0 +1,96 @@
# 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`.

155
docs/windows-servere.md Normal file
View file

@ -0,0 +1,155 @@
# 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 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).
// 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).
//
// Everything is SQL Server 2005 syntax (no CONCAT, IIF or STRING_AGG): AX 3.0 typically runs on 2000 or 2005.
[
@ -13,7 +13,8 @@
"title": "Dynamics AX 3.0 er uden support fra Microsoft",
"severity": "Medium",
"scope": "Database",
"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",
"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",
"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.",
@ -26,7 +27,8 @@
"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",
"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",
"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",
"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}.",
@ -39,7 +41,8 @@
"title": "Dynamics AX-database på en instans, hvor MAXDOP ikke er 1 (SQL Server 2005-2014)",
"severity": "Medium",
"scope": "Database",
"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')",
"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",
"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.",
@ -55,8 +58,9 @@
"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 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')",
"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",
"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).",
@ -72,7 +76,8 @@
"title": "Dynamics AX-tabeller uden unikt RecId-indeks",
"severity": "Medium",
"scope": "Database",
"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",
"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",
"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}.",
@ -85,7 +90,8 @@
"title": "Store Dynamics AX-tabeller uden clustered index (heaps)",
"severity": "Medium",
"scope": "Database",
"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",
"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",
"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}.",
@ -98,7 +104,8 @@
"title": "Automatiske statistikker er slået fra i en Dynamics AX-database",
"severity": "High",
"scope": "Database",
"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')",
"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)",
"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,6 +30,8 @@ public sealed class AgentOptions
public TempdbOptions Tempdb { get; set; } = new();
public DynamicsAxOptions DynamicsAx { get; set; } = new();
public IList<MonitoredInstance> Instances { get; } = [];
}
@ -122,3 +124,90 @@ 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,6 +87,9 @@ 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,7 +31,8 @@ public sealed partial class AgentRequestService(
ISqlConnectionFactory connectionFactory,
RepositoryReadiness readiness,
TimeProvider timeProvider,
ILogger<AgentRequestService> logger) : BackgroundService
ILogger<AgentRequestService> logger,
Servers.ServerRequestHandler? serverRequests = null) : 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);
@ -60,7 +61,7 @@ public sealed partial class AgentRequestService(
{
nextHousekeeping = now.AddMinutes(10);
await repository.PublishAgentKeyAsync(key.Describe(now), stoppingToken).ConfigureAwait(false);
await repository.CleanUpAgentRequestsAsync(now, RequestTimeout + TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(30), TimeSpan.FromDays(30), stoppingToken)
await repository.CleanUpAgentRequestsAsync(now, StaleAfter, TimeSpan.FromMinutes(30), TimeSpan.FromDays(30), stoppingToken)
.ConfigureAwait(false);
}
@ -85,8 +86,9 @@ 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(RequestTimeout);
timeout.CancelAfter(limit);
string? result = null;
string? error = null;
try
@ -97,12 +99,13 @@ 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 {RequestTimeout.TotalMinutes:0} minutter.";
error = $"Agenten fik ikke svar inden for {limit.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.
@ -115,6 +118,15 @@ 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,6 +10,9 @@ 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";
@ -45,7 +48,14 @@ public static class SubmissionBuilder
Edition = info.Edition,
ServerName = options.SendServerName ? info.ServerName : null,
},
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(),
// 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(),
Findings = activeFindings.Where(f => Allowed(f.RuleId, f.Subject)).Select(f => new FindingDto
{
RuleId = f.RuleId,

View file

@ -267,6 +267,32 @@ 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)), intervals, connectionFactory, ruleEngine, repository,
settings.Tempdb.VersionStoreMegabytes, settings.Tempdb.SessionMegabytes, settings.DynamicsAx.ToThresholds())), intervals, connectionFactory, ruleEngine, repository,
loggerFactory.CreateLogger($"SQLInsight.Instance.{instance.Name}"), notifications, notificationOptions.Value.WebUrl, hubOutbox);
/// <summary>

View file

@ -116,6 +116,8 @@ 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,6 +14,7 @@
<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

@ -0,0 +1,467 @@
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

@ -0,0 +1,231 @@
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

@ -0,0 +1,139 @@
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

@ -0,0 +1,65 @@
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

@ -0,0 +1,31 @@
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

@ -0,0 +1,367 @@
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,6 +52,47 @@
"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,6 +12,9 @@ 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(),
@ -40,7 +43,10 @@ public static class BuiltInCollectors
new IndexCollector(),
new AvailabilityGroupCollector(),
new AgentJobCollector(),
.. custom is null ? Array.Empty<ICollector>() : [new Custom.CustomQueryCollector(custom)],
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)],
];
}
}
@ -55,6 +61,7 @@ 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,
@ -64,4 +71,5 @@ public sealed record CollectorSettings(
int TransactionLogMegabytes = 1024,
int RequestMinutes = 15,
int VersionStoreMegabytes = 1024,
int TempdbSessionMegabytes = 1024);
int TempdbSessionMegabytes = 1024,
Core.DynamicsAx.AxThresholds? DynamicsAx = null);

View file

@ -29,6 +29,9 @@ 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,
@ -38,7 +41,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
s.last_request_end_time, {contextInfo}
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
@ -52,7 +55,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.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)
@ -73,6 +76,7 @@ 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,16 +51,17 @@ public sealed class DeadlockCollector : Collector<DeadlockSnapshot>
? $"{Db.Literal(lastFile)}, {lastOffset.Value.ToString(CultureInfo.InvariantCulture)}"
: "NULL, NULL";
// Only the deadlock rows carry their XML; the rest are kept just to know where reading stopped.
// 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.
return $"""
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;
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;
""";
}
@ -85,13 +86,16 @@ public sealed class DeadlockCollector : Collector<DeadlockSnapshot>
{
while (await reader.ReadAsync(ct).ConfigureAwait(false))
{
events.Add(reader.GetString(0));
}
if (!reader.IsDBNull(0))
{
events.Add(reader.GetString(0));
}
if (await reader.NextResultAsync(ct).ConfigureAwait(false) && await reader.ReadAsync(ct).ConfigureAwait(false))
{
file = reader.GetString(0);
offset = Convert.ToInt64(reader.GetValue(1), CultureInfo.InvariantCulture);
if (Convert.ToInt64(reader.GetValue(3), CultureInfo.InvariantCulture) == 1)
{
file = reader.GetString(1);
offset = Convert.ToInt64(reader.GetValue(2), CultureInfo.InvariantCulture);
}
}
}

View file

@ -6,9 +6,10 @@ 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.
/// 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.
/// </summary>
public sealed class CustomQueryCollector(CustomRuleCatalog catalog) : Collector<CustomQueryResults>
public sealed class CustomQueryCollector(CustomRuleCatalog catalog, DynamicsAx.AxDatabaseDetector? ax = null) : 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];
@ -23,6 +24,8 @@ public sealed class CustomQueryCollector(CustomRuleCatalog catalog) : Collector<
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);
@ -37,6 +40,7 @@ public sealed class CustomQueryCollector(CustomRuleCatalog catalog) : Collector<
try
{
List<string>? databases = null;
HashSet<string>? axDatabases = null;
foreach (var rule in rules)
{
if (rule.Scope == CustomRuleScope.Server)
@ -47,6 +51,19 @@ public sealed class CustomQueryCollector(CustomRuleCatalog catalog) : Collector<
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,11 +11,14 @@ internal static class Db
{
public const int CommandTimeoutSeconds = 30;
public static async Task<List<T>> QueryAsync<T>(DbConnection connection, string sql, Func<Row, T> map, CancellationToken cancellationToken)
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)
{
await using var command = connection.CreateCommand();
command.CommandText = sql;
command.CommandTimeout = CommandTimeoutSeconds;
command.CommandTimeout = timeoutSeconds;
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
var rows = new List<T>();
var row = new Row(reader);
@ -37,6 +40,32 @@ 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) + "]";
@ -105,4 +134,21 @@ 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,5 +1,6 @@
using System.Data.Common;
using SQLInsight.Core.Configuration;
using SQLInsight.Core.DynamicsAx;
using SQLInsight.Core.Instances;
namespace SQLInsight.Collectors.Discovery;
@ -7,7 +8,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: the session never changes database
/// sys.columns, or their SQL Server 2000 counterparts), through three-part names and [db].sys.sp_executesql: 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
@ -37,33 +38,16 @@ public static class DatabaseDiscovery
""";
/// <summary>
/// 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.
/// 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.
/// </summary>
public static string DynamicsAxSql(string database, SqlServerVersion version)
{
ArgumentNullException.ThrowIfNull(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')
""";
return version.Major >= 9 ? AxSql.InDatabase(database, AxDetection.MetadataSql) : AxDetection.MetadataSql2000(database);
}
/// <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);
@ -82,6 +66,7 @@ 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];
@ -92,11 +77,12 @@ public static class DatabaseDiscovery
try
{
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)
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)
{
databases[i] = database with { IsDynamicsAx = true, DynamicsAxVersion = version };
databases[i] = database with { IsDynamicsAx = true, DynamicsAxVersion = AxVersions.ShortText(ax.Version) };
}
}
catch (DbException)

View file

@ -0,0 +1,167 @@
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

@ -0,0 +1,160 @@
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

@ -0,0 +1,105 @@
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

@ -0,0 +1,19 @@
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,6 +25,8 @@ 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,12 +28,15 @@ 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));
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)));
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 =
"SET LOCK_TIMEOUT 5000; SET DEADLOCK_PRIORITY LOW; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;";
Core.Instances.AgentSession.Settings;
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>The Dynamics AX system tables (SQLDICTIONARY, DATAAREA, SYSCONFIG) are there.</summary>
/// <summary>Recognised as Dynamics AX by the central detection (Core/DynamicsAx/AxDetection.cs).</summary>
public bool IsDynamicsAx { get; init; }
/// <summary>"3.0" (32-bit RECID) or "4.0+" (64-bit RECID), when <see cref="IsDynamicsAx"/>.</summary>
/// <summary>The AX version as <see cref="DynamicsAx.AxVersions.ShortText"/> ("3.0", "2009", "2012 R2/R3" …), when <see cref="IsDynamicsAx"/>.</summary>
public string? DynamicsAxVersion { get; init; }
}

View file

@ -68,15 +68,42 @@ 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: ";
public static bool IsDynamicsAxRule(string ruleId) => ruleId?.StartsWith(RulePrefix, StringComparison.OrdinalIgnoreCase) == true;
/// <summary>Separates the database from the rest of an AX finding's subject: <c>database: AX30 · batch: 4711</c>.</summary>
public const string SubjectSeparator = " · ";
/// <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;
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);
}
}

View file

@ -52,6 +52,12 @@ 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";
@ -92,6 +98,11 @@ 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));
@ -101,28 +112,10 @@ public sealed partial record CustomRuleDefinition
}
/// <summary>
/// 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).
/// 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).
/// </summary>
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).";
}
}
public static IEnumerable<string> QueryProblems(string query) => ReadOnlyQuery.Problems(query);
/// <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)
@ -135,15 +128,6 @@ 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();
}
@ -245,4 +229,6 @@ 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>
public sealed record CustomQueryResult(IReadOnlyList<IReadOnlyDictionary<string, string?>> Rows, string? Error = null, bool PermissionDenied = false, bool Truncated = false);
/// <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);

View file

@ -0,0 +1,186 @@
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,4 +45,10 @@ 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

@ -0,0 +1,226 @@
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

@ -0,0 +1,99 @@
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

@ -0,0 +1,154 @@
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

@ -0,0 +1,73 @@
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