push
This commit is contained in:
parent
0126ae01b4
commit
7471afc79a
|
|
@ -1,5 +1,35 @@
|
||||||
# ASSESSOR – Umsetzungsplan
|
# ASSESSOR – Umsetzungsplan
|
||||||
|
|
||||||
|
## Results cluster foreground correction (2026-10-13, Phase 7 Presentation, 2.25.6)
|
||||||
|
|
||||||
|
Current user consent: Presentation, Platform, Core Tests. User reports minimized Results headers overlapping an open Results body. The shared activation sequence introduced independent layers for each miniature, violating the established cluster contract that the active Result covers its minimized siblings and exposes their narrow color tabs only. Correct the layering rather than move or pad the body: DatasetViewer publishes one shared activation layer for coupled Results; the active Result occupies the reserved next integer above its siblings. The shared layer allocator reserves two integers per activation, preserving relative activation with other floating windows. Pointer and keyboard activation raise the cluster together. Maximized coupled Results retain their cluster foreground layer; detached Results regain independent activation. Geometry, original evidence, scrolling, rightmost close X, filter uppermost role and other windows remain unchanged.
|
||||||
|
|
||||||
|
Permanent synthetic Light/Dark interaction tests restore each of three Results repeatedly, focus minimized controls, activate another floating window and reactivate the cluster, then maximize the active Result. All 56 targeted Results/layout/header/filter/Viewer/version/API checks pass, including 15 Results layout checks. Browser acceptance with the real components and synthetic evidence verifies keyboard restoration in Light/Dark: active layer 3011/3013 is above both minimized siblings at 3010/3012, and hit testing at the beginning of the body reaches the active Result. Screenshot saved outside the repository; temporary preview file/tab removed. Local-only, design-language and diff checks pass. Product patch decision 2.25.6 synchronized in package/lock and UI/API contracts. First mandatory import gate passes all 507 tests; the build then caught a test-only required children prop, corrected without changing assertions or production behavior. Final repeat passes all 507 mandatory import checks and both client/server production builds; existing client chunk-size warning only. The corrected test rerun passes all 15 cases. Backend health confirms 2.25.6, PostgreSQL and unchanged repository revision 592; app HTTP is 200. Release batch verified. No import baseline, data, scoring, database or Report Engine change; no full-suite or phase-completion claim or commit.
|
||||||
|
|
||||||
|
## Security Report centered half-width surface (2026-10-13, Phase 7 Report Engine, 2.25.5)
|
||||||
|
|
||||||
|
User replaces the previous desktop two-thirds report width with half the viewport width, centered. Renderer 1.4.2 uses `min(snapshot width, 50vw)` and retains automatic equal horizontal margins, accepted typography, environment spacing and native popup controls. The existing narrow-window fallback remains readable below 900px. Newly generated reports receive this layout; historical files are unchanged. All five renderer tests pass; engine acceptance protects the half-width expression and centering in both themes. Product patch 2.25.5 and the Core export width assertion await current Platform/Core Tests consent; no Core file changed in this step. No release-build or phase-completion claim.
|
||||||
|
|
||||||
|
Release follow-up: user supplies current `Core-Freigabe: Platform, Core Tests`. Package/lock and UI/API contracts are synchronized to 2.25.5, and the existing synthetic export interaction assertion protects the requested half-width in both themes. All 23 targeted export/renderer/version/API checks pass, as do all 507 mandatory import compatibility checks and the local-only/design-language checks. Backend health reports 2.25.5, PostgreSQL and unchanged repository revision 592; app HTTP is 200. Both client/server production builds pass; existing client chunk-size warning only. Release batch verified; no full-suite or phase-completion claim or commit. No database, source values, scoring, import baseline, Core Presentation or historical report change.
|
||||||
|
|
||||||
|
## Security Report environment spacing reference (2026-10-13, Phase 7 Report Engine)
|
||||||
|
|
||||||
|
User requests the spacing and Results-button size from supplied image 2 for the report environment block. Renderer-only correction keeps the accepted 12px data / 14px prose and both themes, using the original Security data-row rhythm: line height derives from the shared 14px role and a one-line row has a minimum height of that rhythm plus the shared spacing/separator. Two-column separation stays space-5 (20px), group separation space-1 (4px), and group headers align to the shared button height. Results retains its existing compact results-btn padding, shared 28px height, 3px radius and 13px button role. No global tokens, source component, data, popup behavior or Core file change. Renderer 1.4.1; read-port snapshot stays 1.2.0.
|
||||||
|
|
||||||
|
Verified: five renderer checks and all six existing synthetic Light/Dark export interactions pass. Initial concurrent execution hit the first export test's existing five-second limit and affected following act cases; isolated unchanged repeats pass without altered timeouts or assertions. Browser measurement in both themes confirms approximately 61.3px Results width, 28px height, original 6px horizontal padding, 21.8px row pitch/height, 16.8px line height, 20px column gap and 4px group gap. Source data font remains 12px. A first preview exposed an overly wide 12px general-button padding; the final code inherits the original compact Results variant instead. Temporary preview tab/server removed; synthetic reports stay outside the repository.
|
||||||
|
|
||||||
|
Version reassessment: the correction warrants patch 2.25.4. The initial image-only request did not release Core; the verified renderer step therefore initially kept product 2.25.3 pending consent. User subsequently supplies current `Core-Freigabe: Platform, Core Tests`. Package/lock and UI/API version contracts are synchronized to 2.25.4; all 17 version/API/renderer checks pass, along with local-only and design checks. The first mandatory import gate reached the existing five-second timeout in one Node runtime test (506 of 507 passed); its full unchanged repeat passes all 507 checks without modified assertions, fixtures, snapshots or timeouts. Both client/server production builds pass; existing client chunk-size warning only. Backend health confirms 2.25.4, PostgreSQL and unchanged repository revision 592; app HTTP is 200 and the release diff check passes. This release batch is verified; no full-suite or phase-completion claim, commit, Core Presentation, data, database, scanner contract or scoring change. A new export is required; old reports retain their original layout.
|
||||||
|
|
||||||
|
## Security Report header, environment overview and native theme controls (2026-10-13, Phase 7 Presentation / Report Engine)
|
||||||
|
|
||||||
|
Current user release: Presentation, Platform, Core Tests. User requests the complete environment block above Security Findings with a suitable heading, removal of the export-operation explanation, smaller type, a German numerical overview beneath versions, a Security Report heading with separate client/assessment lines and a German assessment date, plus Dark/System/Light controls. The preceding proposed 12px data / 14px prose exception is accepted for this report only; application type roles and all source values remain unchanged.
|
||||||
|
|
||||||
|
The existing read-only Core snapshot adapter publishes the same complete environment markup separately, removing it only from its disposable clone. The Report Engine places it once under System- und Umgebungsübersicht before the Findings panel. All original Results/native-table triggers retain their targets and complete evidence. The read port receives existing Security domain/category counts and structured selected assessment metadata; it reads no additional findings or Result DB material. The German paragraph reports the authoritative deviation total, nonempty domains/problem fields, largest domain/count/share and largest category/count. It rejects a distribution whose exclusive partitions do not reconcile, and explicitly states a zero-deviation assessment. Labels are escaped; no verdict, score, category membership or inferred security priority is created. Assessment names/dates resolve from both direct assessment selection and the assessments of selected scan runs; ISO dates are presented in German, timestamps use Europe/Berlin and unavailable/invalid dates are disclosed rather than invented. A conventional Assessment YYYY-MM-DD name does not repeat its date.
|
||||||
|
|
||||||
|
Security Report replaces Security Export in the document title/header. Client and each selected Assessment have separate lines; the previously generated operating instructions are removed. Native radio controls use the same three glyphs and theme declarations as the app. CSS :has selects Dark/Light and prefers-color-scheme selects System; the selection applies to the complete report and every standalone popup, keeps the existing amber role and keyboard focus, and uses no JavaScript, external resource or app preference/storage mutation. Initial selection retains the exported source theme. The report keeps its centered responsive two-thirds surface, Technical Report background, native category opening, popup scroll containment, maximization and rightmost X. Snapshot read port 1.2.0 / renderer 1.4.0; optional added fields retain old read-port support. Product patch decision 2.25.3, synchronized package/lock and UI/API version contracts; archived/downloaded files are not rewritten.
|
||||||
|
|
||||||
|
Verification: 38 distinct targeted renderer, actual Light/Dark export-click, report/Core/Context boundary/invariance and UI/API version checks pass across the focused runs, along with local-only and design checks. Synthetic interaction coverage retains complete 1000-row native tables, exact linked targets, immutable source data, complete scope despite Work/Graph filters, revision guards and Report-off late suppression. New interactions protect environment-before-Findings ordering/uniqueness, exact client/assessment/date lines for direct and scan-only selection, summary counts and theme radio selection. Renderer checks cover German dates/timezone, invalid/unknown dates, zero population, reconciliation rejection and escaped labels. Synthetic browser acceptance confirms environment once above Findings, data and graph labels 12px, prose 14px, no page-level horizontal overflow, Light/Dark/System switches using shared colors including popups, and retained exact category disclosure. System follows the current light system preference; a file originally exported in Dark also switches correctly to Light. Temporary preview tab/server removed; synthetic artifacts remain outside the repository. Final repeat after the scan-only metadata correction passes all 507 mandatory import checks and both production builds; existing client chunk-size warning only. Diff check passes. Running backend health confirms 2.25.3, PostgreSQL and unchanged repository revision 592; app HTTP is 200. This release batch is verified. No database/schema/import-baseline/scoring change, full-suite, phase-completion claim or commit. A fresh export is required; existing downloaded and archived reports retain their original contents.
|
||||||
|
|
||||||
## Security export typography matching (2026-10-13, Phase 7 Report Engine)
|
## Security export typography matching (2026-10-13, Phase 7 Report Engine)
|
||||||
|
|
||||||
User correction: reduce popup text and match the complete report's type sizes to the application Security panel. The previous whole-report CSS zoom shrank every application type role and caused the large difference between report and unscaled popups. Renderer 1.3.0 removes this whole-report zoom and uses a responsive centered two-thirds width, retaining the full-width narrow-viewport fallback. Native Security roles remain 14px for structured data/category labels and 16px for context/finding prose; popup prose, fact values and scoring captions now use the existing 14px structured-data role. Popup headings keep the 16px title role. No global application tokens or Core components/styles change.
|
User correction: reduce popup text and match the complete report's type sizes to the application Security panel. The previous whole-report CSS zoom shrank every application type role and caused the large difference between report and unscaled popups. Renderer 1.3.0 removes this whole-report zoom and uses a responsive centered two-thirds width, retaining the full-width narrow-viewport fallback. Native Security roles remain 14px for structured data/category labels and 16px for context/finding prose; popup prose, fact values and scoring captions now use the existing 14px structured-data role. Popup headings keep the 16px title role. No global application tokens or Core components/styles change.
|
||||||
|
|
@ -10,6 +40,12 @@ Verified engine step: nine renderer/boundary/invariance checks pass. Synthetic b
|
||||||
|
|
||||||
Release follow-up: user supplies the complete current `Core-Freigabe: Platform, Core Tests` on 2026-10-13. The old Core export interaction assertions for scaled fixed-width markup/zoom now require the responsive width, absence of zoom, native SVG width and 14px popup roles after the actual export click; every original source/count/membership/disclosure/CSP assertion remains. Product patch 2.25.2 is synchronized in package/lock and UI/API version contracts. All 25 targeted export/renderer/boundary/invariance/version/API checks pass. Local-only and design checks pass from the verified engine step, and the final diff check passes. Running backend health reports 2.25.2, PostgreSQL and unchanged repository revision 592; app HTTP is 200. Final mandatory gate passes all 507 import checks and both client/server production builds; existing client chunk-size warning only. This release batch is verified; no full-suite or phase-completion claim, commit, database, source data, score or archived-report changes. A fresh Security export is required to receive the typography correction.
|
Release follow-up: user supplies the complete current `Core-Freigabe: Platform, Core Tests` on 2026-10-13. The old Core export interaction assertions for scaled fixed-width markup/zoom now require the responsive width, absence of zoom, native SVG width and 14px popup roles after the actual export click; every original source/count/membership/disclosure/CSP assertion remains. Product patch 2.25.2 is synchronized in package/lock and UI/API version contracts. All 25 targeted export/renderer/boundary/invariance/version/API checks pass. Local-only and design checks pass from the verified engine step, and the final diff check passes. Running backend health reports 2.25.2, PostgreSQL and unchanged repository revision 592; app HTTP is 200. Final mandatory gate passes all 507 import checks and both client/server production builds; existing client chunk-size warning only. This release batch is verified; no full-suite or phase-completion claim, commit, database, source data, score or archived-report changes. A fresh Security export is required to receive the typography correction.
|
||||||
|
|
||||||
|
## Maester first-read Problem ends before a list (2026-10-13, Phase 7 Presentation)
|
||||||
|
|
||||||
|
Regression of the 2026-09-28 agreement (first prose paragraph of the native Maester result is the `Problem`; tables, headings and the rest stay in Results): a list directly following the statement, either on its own lines or flattened behind the introducing colon by the Excel export (`… excluded: - [Policy](…)`), was appended to the Problem block. `resultLead()` now also ends the statement at a list marker at line start or after a colon; a marker inside a sentence (`access * user`) stays prose, and a result that opens with a list yields no Problem line instead of promoting later text. User released `Core-Freigabe: Presentation, Core Tests`. Permanent synthetic Finding Reader tests cover both export variants and the list-first case. Verified: 54 Finding Reader/first-read checks and all 507 mandatory import checks (via `npm test`); the full suite reports nine failures in Data Hub, DB Structure, Pathfinder, Context facet and Client Info button tests that do not use `first-read.ts` and come from other uncommitted work in the tree. No source data, Results content, scoring or import baseline changes. Version reassessment: a patch correction is warranted but pending a Platform release; batch it with the next release step.
|
||||||
|
|
||||||
|
Change log 2026-10-13: Maester Problem statement stops before a following list.
|
||||||
|
|
||||||
## Native Security export interaction and layout corrections (2026-10-13, Phase 7 Report Engine)
|
## Native Security export interaction and layout corrections (2026-10-13, Phase 7 Report Engine)
|
||||||
|
|
||||||
User requests isolated popup/page scrolling, popup maximization, compact top facts, category click selection/disclosure and a centered two-thirds report with the Technical Report background. User explicitly retains the no-JavaScript export contract. Changes are confined to the Report Engine renderer and its tests; the Core snapshot adapter and source styles remain unchanged. Native checkbox controls maximize/restore each popup before the rightmost close X. Popup scroll containers contain overscroll, including nested source popups. Top-level popup fact lists use five columns, reducing to three/two on narrow windows. Graph links target a descendant inside each native category details element so the browser reveals the closed ancestor; the target category summary carries the existing amber selection role. The report preserves its logical source width while fitting two thirds of the desktop viewport; narrow windows use the available width. Body, report, Security panel and Findings table use the Technical Report surface-panel background. Renderer version 1.2.0; snapshot schema remains 1.1.0. Existing downloaded reports require a fresh export.
|
User requests isolated popup/page scrolling, popup maximization, compact top facts, category click selection/disclosure and a centered two-thirds report with the Technical Report background. User explicitly retains the no-JavaScript export contract. Changes are confined to the Report Engine renderer and its tests; the Core snapshot adapter and source styles remain unchanged. Native checkbox controls maximize/restore each popup before the rightmost close X. Popup scroll containers contain overscroll, including nested source popups. Top-level popup fact lists use five columns, reducing to three/two on narrow windows. Graph links target a descendant inside each native category details element so the browser reveals the closed ancestor; the target category summary carries the existing amber selection role. The report preserves its logical source width while fitting two thirds of the desktop viewport; narrow windows use the available width. Body, report, Security panel and Findings table use the Technical Report surface-panel background. Renderer version 1.2.0; snapshot schema remains 1.1.0. Existing downloaded reports require a fresh export.
|
||||||
|
|
@ -22,6 +58,18 @@ Design-language and diff checks pass. The local-only word-boundary guard initial
|
||||||
|
|
||||||
Change log 2026-10-13: preserve script-free export while adding native maximization, category revelation, scroll containment, compact facts and centered Technical Report surfaces.
|
Change log 2026-10-13: preserve script-free export while adding native maximization, category revelation, scroll containment, compact facts and centered Technical Report surfaces.
|
||||||
|
|
||||||
|
## Context catalog 1.12.0 / vocabulary 1.3.0 / engine 1.13.0: proactive cataloguing (2026-10-13, Phase 13E Context Engine)
|
||||||
|
|
||||||
|
Engine-only follow-up on the user's request to catalogue further known gaps in advance. Source of exact rule IDs and native texts: `docs/analysis/context-pass-gaps-dummy-2026-10-04.md` (released Dummy client) plus Microsoft-baseline GPO keys. Added 47 conditions with favouring statements and sources (`techniqueIds` left empty so declared-technique assignment of other findings is unchanged), 114 exact scanner rules (Purple Knight SI-IDs, Maester EIDSCA/MT/CISA/CIS IDs, CIS-CAT 19.7.5.1 with title guard), 12 CIS-CAT section titles (Password Policy, Account Lockout Policy, Domain member, Microsoft network client/server, Network access, User Account Control, System objects, Shutdown, Devices, Attachment Manager, Playback) and 88 GPO verdict rules (further user rights, Registry Values for SMB/secure channel/NTLM/anonymous access/LM hash/UAC/Kerberos types/sign-in screen, and policy keys for PowerShell, EventLog, Terminal Services, WinRM, LanmanWorkstation, SMB1, Rpc, Installer, DeviceGuard, CredentialsDelegation, WDigest, Printers, Defender, Edge, Explorer, SmartScreen values, Office 16.0 user tree, DeviceInstall, FVE, Kernel DMA, Personalization). Value-specific rules keep precedence (for example `AllowDomainPINLogon` vs. `EnableSmartScreen` under `Windows\System`).
|
||||||
|
|
||||||
|
Alignment with existing decisions, measured by the precision guards: trusts are placed in `AD Recovery Gaps` (user decision 2026-10-12); the printer policy tree stays in credential relay as in the Microsoft baseline area table; SDProp/adminCount, Reanimate-Tombstones and Ephemeral Admins follow the vocabulary's persistence reading; DCOM users go to lateral movement; MT.1035 (RMAU) to cloud privileged access. Vocabulary 1.3.0 narrows ten terms where catalogued texts would otherwise be contradicted (gMSA password access/age, admin old passwords, trust old passwords, Server Trust Account, trust SID history, guest MFA, admin MFA, Global Administrator alerts, adminCount) and adds eight distinctive terms for the new rules; recognition precision stays contradiction-free and recall stays at or above 70 %. Engine 1.13.0 so stored Context/work-package snapshots refresh. Synthetic engine test covers each rule kind. Count expectations in Context engine/UI tests updated for the larger clusters; the synthetic "uncatalogued" Maester fixture moved from `MT.1001` (now catalogued) to `MT.0001`.
|
||||||
|
|
||||||
|
With `Core-Freigabe: Core Tests` (2026-10-13) the credential-relay count in `tests/server/assessor-http-server.test.ts:203` moved from `unknown: 26` to `31`; all 41 server test files pass. Nine further `tests/features` failures (Client Info colors, Data Hub, DB Structure, Pathfinder text, source selection, two Context facet order checks) already fail on the pre-session state and are unrelated to this change. No verdict, score, Work count or Result DB change; no product-version step.
|
||||||
|
|
||||||
|
## Context catalog 1.11.0: convenience PIN and redirected default OU (2026-10-13, Phase 13E Context Engine)
|
||||||
|
|
||||||
|
Engine-only catalog addition; no Core path changed. User reported two deviations that still landed in `Other Deviations (not yet catalogued)`. (1) GPO `HKLM\Software\Policies\Microsoft\Windows\System\AllowDomainPINLogon` (Turn on convenience PIN sign-in): a GPO verdict rule on that exact value name maps the authoritative rolled-up baseline verdict to the new condition `gpo-convenience-pin-sign-in-deviates` in `Physical & Local Access Exposure`, consistent with the CIS-CAT `Logon` section that already maps there; sibling values under the same key stay uncatalogued. (2) PingCastle `S-DefaultOUChanged` (exact rule ID, `fail-means-met`) maps to the new standalone condition `ad-default-container-redirected` in `Detection Blind Spots`, because the scan report itself states that audit programs may not check the moved objects; no stronger claim is made. Both relationships are phrased as favouring with cited sources. Verdicts, scores, Work counts and Result DB stay unchanged. Verified: new synthetic engine test plus all Context, Remediation and Report suites (227 tests). Existing Context snapshots show the new assignment after the next Context run; no reimport. No product-version step (engine catalog content only, batched with the next release).
|
||||||
|
|
||||||
## Complete HTML Security view export (2026-10-12, Phase 7 Presentation / Report Engine)
|
## Complete HTML Security view export (2026-10-12, Phase 7 Presentation / Report Engine)
|
||||||
|
|
||||||
Current release: Presentation, Core Tests, Platform. User requests the complete Security tab with its current width/scaling, ASSESSOR logo, client/assessment and version metadata like the Technical Export, and a dominant VERTRAULICH notice. The existing Security view is rendered through a disposable Core Presentation snapshot adapter at its measured width, using the same components, supplied counts/membership and CSS. A declared read-only SecurityViewReadPort publishes only the complete authorized view markup and metadata to the Report Engine's platform-neutral standalone HTML renderer. The existing Dashboard report extension point remains the sole Core consumer. No new classification, scoring, database access or report schema; the snapshot reads no additional Result DB material beyond what the Security view displays.
|
Current release: Presentation, Core Tests, Platform. User requests the complete Security tab with its current width/scaling, ASSESSOR logo, client/assessment and version metadata like the Technical Export, and a dominant VERTRAULICH notice. The existing Security view is rendered through a disposable Core Presentation snapshot adapter at its measured width, using the same components, supplied counts/membership and CSS. A declared read-only SecurityViewReadPort publishes only the complete authorized view markup and metadata to the Report Engine's platform-neutral standalone HTML renderer. The existing Dashboard report extension point remains the sole Core consumer. No new classification, scoring, database access or report schema; the snapshot reads no additional Result DB material beyond what the Security view displays.
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "assessor",
|
"name": "assessor",
|
||||||
"version": "2.25.2",
|
"version": "2.25.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "assessor",
|
"name": "assessor",
|
||||||
"version": "2.25.2",
|
"version": "2.25.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "^5.3.0",
|
"@fontsource-variable/inter": "^5.3.0",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "assessor",
|
"name": "assessor",
|
||||||
"version": "2.25.2",
|
"version": "2.25.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* `not semantically mapped` and is never guessed. Conditions are defined by explicit value sets;
|
* `not semantically mapped` and is never guessed. Conditions are defined by explicit value sets;
|
||||||
* every other value is `unknown`. Nothing here is a compliance verdict or an effective host state.
|
* every other value is `unknown`. Nothing here is a compliance verdict or an effective host state.
|
||||||
*/
|
*/
|
||||||
export const ASSESSMENT_CONTEXT_CATALOG = { id: 'assessment-context-catalog', version: '1.10.0' } as const;
|
export const ASSESSMENT_CONTEXT_CATALOG = { id: 'assessment-context-catalog', version: '1.12.0' } as const;
|
||||||
|
|
||||||
export interface ContextSource {
|
export interface ContextSource {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -413,6 +413,7 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
{ id: 'process-creation-not-logged', label: { de: "Prozessstarts oder ihre Befehlszeilen werden nicht aufgezeichnet", en: "Process creation or its command line is not recorded" } },
|
{ id: 'process-creation-not-logged', label: { de: "Prozessstarts oder ihre Befehlszeilen werden nicht aufgezeichnet", en: "Process creation or its command line is not recorded" } },
|
||||||
{ id: 'security-event-log-small', label: { de: "Das Sicherheitsprotokoll ist kleiner als empfohlen oder wird überschrieben", en: "The security event log is smaller than recommended or is overwritten" } },
|
{ id: 'security-event-log-small', label: { de: "Das Sicherheitsprotokoll ist kleiner als empfohlen oder wird überschrieben", en: "The security event log is smaller than recommended or is overwritten" } },
|
||||||
{ id: 'ntlm-usage-not-audited', label: { de: "Die NTLM-Nutzung wird nicht überwacht", en: "NTLM usage is not audited" } },
|
{ id: 'ntlm-usage-not-audited', label: { de: "Die NTLM-Nutzung wird nicht überwacht", en: "NTLM usage is not audited" } },
|
||||||
|
{ id: 'ad-default-container-redirected', label: { de: "Der Standardort für neue Benutzer- oder Computerobjekte (wellKnownObjects) wurde geändert", en: "The default location for new user or computer objects (wellKnownObjects) was changed" } },
|
||||||
{ id: 'firewall-logging-off', label: { de: "Die Windows-Firewall protokolliert verworfene oder erfolgreiche Verbindungen nicht", en: "The Windows firewall does not log dropped or successful connections" } },
|
{ id: 'firewall-logging-off', label: { de: "Die Windows-Firewall protokolliert verworfene oder erfolgreiche Verbindungen nicht", en: "The Windows firewall does not log dropped or successful connections" } },
|
||||||
{ id: 'identity-threat-detection-missing', label: { de: "Defender for Identity ist auf Domain Controllern nicht bereitgestellt", en: "Defender for Identity is not deployed on domain controllers" } },
|
{ id: 'identity-threat-detection-missing', label: { de: "Defender for Identity ist auf Domain Controllern nicht bereitgestellt", en: "Defender for Identity is not deployed on domain controllers" } },
|
||||||
{ id: 'cloud-security-logs-not-forwarded', label: { de: "Entra-Sicherheitsprotokolle werden nicht an eine Überwachung weitergeleitet", en: "Entra security logs are not sent to a monitoring service" } },
|
{ id: 'cloud-security-logs-not-forwarded', label: { de: "Entra-Sicherheitsprotokolle werden nicht an eine Überwachung weitergeleitet", en: "Entra security logs are not sent to a monitoring service" } },
|
||||||
|
|
@ -539,6 +540,54 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
{ id: 'gpo-reversible-password-storage-deviates', label: { de: "Die Speicherung von Kennwörtern mit umkehrbarer Verschlüsselung weicht in der GPO von der Microsoft-Baseline ab", en: "Storing passwords with reversible encryption deviates from the Microsoft baseline in the GPO" } },
|
{ id: 'gpo-reversible-password-storage-deviates', label: { de: "Die Speicherung von Kennwörtern mit umkehrbarer Verschlüsselung weicht in der GPO von der Microsoft-Baseline ab", en: "Storing passwords with reversible encryption deviates from the Microsoft baseline in the GPO" } },
|
||||||
{ id: 'gpo-logon-hours-enforcement-deviates', label: { de: "Das Abmelden nach Ablauf der Anmeldezeiten weicht in der GPO von der Microsoft-Baseline ab", en: "Forcing logoff when logon hours expire deviates from the Microsoft baseline in the GPO" } },
|
{ id: 'gpo-logon-hours-enforcement-deviates', label: { de: "Das Abmelden nach Ablauf der Anmeldezeiten weicht in der GPO von der Microsoft-Baseline ab", en: "Forcing logoff when logon hours expire deviates from the Microsoft baseline in the GPO" } },
|
||||||
{ id: 'gpo-office-forms-auth-hosts-deviate', label: { de: "Die Office-Hosts für formularbasierte Anmeldung weichen von der Microsoft-Baseline ab", en: "The Office hosts allowed for forms-based sign-in deviate from the Microsoft baseline" } },
|
{ id: 'gpo-office-forms-auth-hosts-deviate', label: { de: "Die Office-Hosts für formularbasierte Anmeldung weichen von der Microsoft-Baseline ab", en: "The Office hosts allowed for forms-based sign-in deviate from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-convenience-pin-sign-in-deviates', label: { de: "Die Anmeldung mit Komfort-PIN weicht in der GPO von der Microsoft-Baseline ab", en: "Convenience PIN sign-in deviates from the Microsoft baseline in the GPO" } },
|
||||||
|
{ id: 'ad-dcshadow-evidence', label: { de: "Es gibt Hinweise auf einen DCShadow-Angriff", en: "There is evidence of a DCShadow attack" } },
|
||||||
|
{ id: 'ad-primary-group-anomaly', label: { de: "Benutzer oder Computer haben eine abweichende oder nicht lesbare primäre Gruppe", en: "Users or computers have a non-default or unreadable primary group" } },
|
||||||
|
{ id: 'krbtgt-password-old', label: { de: "Das Kennwort des KRBTGT-Kontos ist alt", en: "The KRBTGT account password is old" } },
|
||||||
|
{ id: 'ad-sidhistory-abuse', label: { de: "SIDHistory enthält privilegierte SIDs oder wurde kürzlich geändert", en: "SIDHistory contains privileged SIDs or was changed recently" } },
|
||||||
|
{ id: 'ad-recent-sensitive-changes', label: { de: "Kürzliche Änderungen an privilegierten Gruppen, Standardrichtlinien, Schema oder SYSVOL wurden festgestellt", en: "Recent changes to privileged groups, default policies, schema or SYSVOL were recorded" } },
|
||||||
|
{ id: 'ad-dangerous-acl-delegation', label: { de: "Nicht standardmäßige Prinzipale haben gefährliche Rechte oder Besitz an sensiblen AD-Objekten", en: "Non-default principals hold dangerous rights on, or own, sensitive AD objects" } },
|
||||||
|
{ id: 'rodc-credential-caching-risky', label: { de: "Read-Only Domain Controller speichern Anmeldedaten privilegierter oder zu vieler Konten", en: "Read-only domain controllers cache credentials of privileged or too many accounts" } },
|
||||||
|
{ id: 'gpo-link-delegation-broad', label: { de: "Das Verknüpfen von GPOs auf Domäne, Domain-Controller-OU oder Standort ist delegiert", en: "Linking GPOs at the domain, domain controller OU or site level is delegated" } },
|
||||||
|
{ id: 'ad-privileged-membership-hygiene', label: { de: "Privilegierte Gruppen enthalten inaktive, deaktivierte, unerwartete oder zu viele Mitglieder", en: "Privileged groups contain inactive, disabled, unexpected or too many members" } },
|
||||||
|
{ id: 'ad-asrep-roastable', label: { de: "Benutzerkonten benötigen keine Kerberos-Vorauthentifizierung", en: "User accounts do not require Kerberos pre-authentication" } },
|
||||||
|
{ id: 'ad-user-spn', label: { de: "Benutzerkonten haben einen Service Principal Name", en: "User accounts have a service principal name defined" } },
|
||||||
|
{ id: 'ad-passwords-old', label: { de: "Kennwörter von Benutzer-, Admin-, Computer-, Vertrauens- oder Dienstkonten sind älter als empfohlen", en: "Passwords of user, admin, computer, trust or service accounts are older than recommended" } },
|
||||||
|
{ id: 'ad-password-not-required', label: { de: "Benutzerkonten benötigen kein Kennwort", en: "User accounts do not require a password" } },
|
||||||
|
{ id: 'ad-recoverable-passwords', label: { de: "Kennwörter werden umkehrbar oder schwach gespeichert (GPO, umkehrbare Verschlüsselung, userPassword, LM-Hash)", en: "Passwords are stored in a recoverable or weak form (GPO, reversible encryption, userPassword, LM hash)" } },
|
||||||
|
{ id: 'ad-obsolete-components', label: { de: "Veraltete Funktionsebenen, Betriebssysteme oder SYSVOL-Replikation (NTFRS) sind im Einsatz", en: "Obsolete functional levels, operating systems or SYSVOL replication (NTFRS) are in use" } },
|
||||||
|
{ id: 'ad-constrained-delegation-risky', label: { de: "Eingeschränkte Delegation, Protokollübergang oder Delegation an verwaiste SPNs ist konfiguriert", en: "Constrained delegation, protocol transition or delegation to ghost SPNs is configured" } },
|
||||||
|
{ id: 'dc-health-inconsistent', label: { de: "Domain Controller sind in inkonsistentem Zustand oder haben sich lange nicht authentifiziert", en: "Domain controllers are in an inconsistent state or have not authenticated for a long time" } },
|
||||||
|
{ id: 'ad-trust-configuration-weak', label: { de: "Vertrauensstellungen erlauben SIDHistory, haben keine Quarantäne, gefährliche Attribute oder alte Kennwörter", en: "Trusts allow SID history, lack quarantine, carry dangerous attributes or have old passwords" } },
|
||||||
|
{ id: 'ad-explicit-certificate-mapping', label: { de: "Konten haben explizite Zertifikatszuordnungen (altSecurityIdentities)", en: "Accounts have explicit certificate mappings (altSecurityIdentities)" } },
|
||||||
|
{ id: 'gpo-dangerous-execution-paths', label: { de: "GPOs verweisen auf beschreibbare Verknüpfungen oder gefährliche Anmeldeskriptpfade", en: "GPOs reference writable shortcuts or dangerous logon script paths" } },
|
||||||
|
{ id: 'weak-certificate-cipher', label: { de: "Ein Zertifikat verwendet ein schwaches Verschlüsselungsverfahren", en: "A certificate uses a weak cipher" } },
|
||||||
|
{ id: 'entra-app-creation-unrestricted', label: { de: "Das Registrieren oder Verwalten von Anwendungen ist nicht eingeschränkt", en: "Registering or managing applications is not restricted" } },
|
||||||
|
{ id: 'entra-pim-controls-weak', label: { de: "Privilegierte Entra-Rollen werden nicht nur über PIM mit Genehmigung vergeben oder aktiviert", en: "Privileged Entra roles are not only assigned or activated through PIM with approval" } },
|
||||||
|
{ id: 'entra-password-expiry-enabled', label: { de: "Kennwörter in Entra laufen ab", en: "Passwords in Entra expire" } },
|
||||||
|
{ id: 'cis-password-policy-unmet', label: { de: "Empfehlungen des Benchmarks zu Kennwort- und Kontosperrungsrichtlinie sind nicht erfüllt", en: "Benchmark password and account lockout policy recommendations are not met" } },
|
||||||
|
{ id: 'cis-anonymous-access-unmet', label: { de: "Empfehlungen des Benchmarks zum anonymen Netzwerkzugriff sind nicht erfüllt", en: "Benchmark anonymous network access recommendations are not met" } },
|
||||||
|
{ id: 'cis-local-privilege-hardening-unmet', label: { de: "Empfehlungen des Benchmarks zu Benutzerkontensteuerung und Systemobjekten sind nicht erfüllt", en: "Benchmark User Account Control and system object recommendations are not met" } },
|
||||||
|
{ id: 'gpo-powershell-logging-deviates', label: { de: "Die GPO-Einstellungen zur PowerShell-Protokollierung weichen von der Microsoft-Baseline ab", en: "The GPO PowerShell logging settings deviate from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-event-logging-deviates', label: { de: "Die GPO-Einstellungen zu Ereignisprotokollen oder zur Befehlszeilenaufzeichnung weichen von der Microsoft-Baseline ab", en: "The GPO event log or command-line recording settings deviate from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-remote-desktop-deviates', label: { de: "Die GPO-Einstellungen zu Remotedesktop und Remoteunterstützung weichen von der Microsoft-Baseline ab", en: "The GPO Remote Desktop and Remote Assistance settings deviate from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-remote-management-deviates', label: { de: "Die GPO-Einstellungen zu WinRM weichen von der Microsoft-Baseline ab", en: "The GPO WinRM settings deviate from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-smb-hardening-deviates', label: { de: "Die GPO-Einstellungen zur SMB-Härtung weichen von der Microsoft-Baseline ab", en: "The GPO SMB hardening settings deviate from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-secure-channel-deviates', label: { de: "Die GPO-Einstellungen zum sicheren Kanal (Netlogon) weichen von der Microsoft-Baseline ab", en: "The GPO secure channel (Netlogon) settings deviate from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-ntlm-hardening-deviates', label: { de: "Die GPO-Einstellungen zur NTLM-Sitzungssicherheit weichen von der Microsoft-Baseline ab", en: "The GPO NTLM session security settings deviate from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-anonymous-access-deviates', label: { de: "Die GPO-Einstellungen zu anonymem Zugriff oder Gastzugang weichen von der Microsoft-Baseline ab", en: "The GPO anonymous or guest access settings deviate from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-lm-hash-storage-deviates', label: { de: "Die GPO-Einstellung zur Speicherung von LM-Hashes weicht von der Microsoft-Baseline ab", en: "The GPO setting for storing LM hashes deviates from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-kerberos-encryption-deviates', label: { de: "Die GPO-Einstellung zu Kerberos-Verschlüsselungstypen weicht von der Microsoft-Baseline ab", en: "The GPO Kerberos encryption types setting deviates from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-local-privilege-hardening-deviates', label: { de: "Die GPO-Einstellungen zu Benutzerkontensteuerung, Systemobjekten oder erhöhter Installation weichen von der Microsoft-Baseline ab", en: "The GPO User Account Control, system object or elevated installation settings deviate from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-credential-protection-deviates', label: { de: "Die GPO-Einstellungen zum Schutz von Anmeldedaten weichen von der Microsoft-Baseline ab", en: "The GPO credential protection settings deviate from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-print-hardening-deviates', label: { de: "Die GPO-Einstellungen zur Druckerhärtung weichen von der Microsoft-Baseline ab", en: "The GPO printer hardening settings deviate from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-malware-protection-deviates', label: { de: "Die GPO-Einstellungen zu Defender, SmartScreen, Browser oder AutoPlay weichen von der Microsoft-Baseline ab", en: "The GPO Defender, SmartScreen, browser or AutoPlay settings deviate from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-office-hardening-deviates', label: { de: "Die GPO-Einstellungen zur Office-Härtung (Makros, DDE, OLE) weichen von der Microsoft-Baseline ab", en: "The GPO Office hardening settings (macros, DDE, OLE) deviate from the Microsoft baseline" } },
|
||||||
|
{ id: 'gpo-local-device-access-deviates', label: { de: "Die GPO-Einstellungen zu Anmeldebildschirm, Geräteinstallation, Laufwerksverschlüsselung oder DMA-Schutz weichen von der Microsoft-Baseline ab", en: "The GPO sign-in screen, device installation, drive encryption or DMA protection settings deviate from the Microsoft baseline" } },
|
||||||
|
{ id: 'ad-tombstone-reanimation-right', label: { de: "Objekte haben das erweiterte Recht Reanimate-Tombstones", en: "Objects hold the Reanimate-Tombstones extended right" } },
|
||||||
|
{ id: 'ad-sdprop-protection-inconsistent', label: { de: "Privilegierte Objekte oder Operator-Gruppen werden nicht einheitlich durch AdminSDHolder und SDProp geschützt", en: "Privileged objects or operator groups are not consistently protected by AdminSDHolder and SDProp" } },
|
||||||
|
{ id: 'dcom-perflog-groups-populated', label: { de: "Die Gruppen Distributed COM Users oder Performance Log Users haben Mitglieder", en: "The Distributed COM Users or Performance Log Users groups have members" } },
|
||||||
|
{ id: 'entra-ca-groups-unprotected', label: { de: "In Conditional Access genutzte Sicherheitsgruppen sind nicht durch RMAU geschützt", en: "Security groups used in Conditional Access are not protected by restricted management administrative units" } },
|
||||||
{ id: 'uncatalogued-deviations-gpo', label: { de: "Eine GPO-Einstellung weicht von der Microsoft-Baseline ab und ist noch keinem Angriffsweg zugeordnet", en: "A GPO setting deviates from the Microsoft baseline and is not yet assigned to an attack path" } }
|
{ id: 'uncatalogued-deviations-gpo', label: { de: "Eine GPO-Einstellung weicht von der Microsoft-Baseline ab und ist noch keinem Angriffsweg zugeordnet", en: "A GPO setting deviates from the Microsoft baseline and is not yet assigned to an attack path" } }
|
||||||
],
|
],
|
||||||
relationships: [
|
relationships: [
|
||||||
|
|
@ -1561,6 +1610,251 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
statement: { de: "Abweichend erlaubte Anmeldeformulare in Office begünstigen, dass Zugangsdaten in fremde Anmeldedialoge eingegeben werden.", en: "Deviating forms-based sign-in hosts in Office favour credentials being entered into foreign sign-in prompts." },
|
statement: { de: "Abweichend erlaubte Anmeldeformulare in Office begünstigen, dass Zugangsdaten in fremde Anmeldedialoge eingegeben werden.", en: "Deviating forms-based sign-in hosts in Office favour credentials being entered into foreign sign-in prompts." },
|
||||||
sources: [T1056_002]
|
sources: [T1056_002]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-convenience-pin-sign-in-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Eine abweichend erlaubte Komfort-PIN begünstigt, dass sich Domänenbenutzer am Gerät mit einer kurzen PIN statt ihres Kennworts anmelden.", en: "A deviating convenience PIN favours domain users signing in on the device with a short PIN instead of their password." },
|
||||||
|
sources: [{ title: 'Policy CSP - CredentialProviders: AllowPINLogon (Turn on convenience PIN sign-in)', url: 'https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-credentialproviders', publisher: 'Microsoft' }, CIS_WINDOWS_SERVER]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-default-container-redirected', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Ein geänderter Standardort für neue Objekte begünstigt, dass Prüfprogramme die verschobenen Objekte nicht prüfen.", en: "A changed default location for new objects favours audit programs not checking the moved objects." },
|
||||||
|
sources: [PINGCASTLE_RULES]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-dcshadow-evidence', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Hinweise auf DCShadow begünstigen, dass unbemerkte Änderungen über einen gefälschten Domain Controller im Verzeichnis verbleiben.", en: "Evidence of DCShadow favours unnoticed changes made through a rogue domain controller remaining in the directory." },
|
||||||
|
sources: [mitre('T1207', 'Rogue Domain Controller'), PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-primary-group-anomaly', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichende primäre Gruppen begünstigen, dass Gruppenmitgliedschaften verdeckt bestehen bleiben.", en: "Non-default primary groups favour group memberships persisting in a concealed way." },
|
||||||
|
sources: [T1098, PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'krbtgt-password-old', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Ein altes KRBTGT-Kennwort begünstigt, dass einmal gestohlenes Schlüsselmaterial weiter für gefälschte Tickets nutzbar bleibt.", en: "An old KRBTGT password favours once-stolen key material remaining usable for forged tickets." },
|
||||||
|
sources: [mitre('T1558.001', 'Steal or Forge Kerberos Tickets: Golden Ticket'), PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-sidhistory-abuse', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Privilegierte oder neu gesetzte SIDHistory-Einträge begünstigen verdeckte, dauerhafte Rechte.", en: "Privileged or newly set SIDHistory entries favour concealed, lasting rights." },
|
||||||
|
sources: [mitre('T1134.005', 'Access Token Manipulation: SID-History Injection'), PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-recent-sensitive-changes', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Kürzliche Änderungen an sensiblen Objekten begünstigen, dass unbemerkte Rechte oder Hintertüren entstanden sind.", en: "Recent changes to sensitive objects favour unnoticed rights or backdoors having been created." },
|
||||||
|
sources: [T1098, PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-dangerous-acl-delegation', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Gefährliche Rechte an sensiblen AD-Objekten begünstigen, dass Konten ohne Adminrechte privilegierte Anmeldedaten erlangen.", en: "Dangerous rights on sensitive AD objects favour accounts without admin rights obtaining privileged credentials." },
|
||||||
|
sources: [mitre('T1003.006', 'OS Credential Dumping: DCSync'), PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'rodc-credential-caching-risky', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Zwischengespeicherte privilegierte Anmeldedaten auf RODCs begünstigen, dass sie mit einem RODC abgegriffen werden.", en: "Cached privileged credentials on RODCs favour them being captured together with an RODC." },
|
||||||
|
sources: [PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-link-delegation-broad', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Delegierte GPO-Verknüpfung auf hoher Ebene begünstigt, dass Nicht-Administratoren Richtlinien auf privilegierte Systeme anwenden.", en: "Delegated GPO linking at a high level favours non-administrators applying policies to privileged systems." },
|
||||||
|
sources: [mitre('T1484.001', 'Domain or Tenant Policy Modification: Group Policy Modification'), PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-privileged-membership-hygiene', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Unnötige Mitglieder privilegierter Gruppen begünstigen, dass mehr Konten als nötig privilegierte Anmeldedaten tragen.", en: "Unnecessary members of privileged groups favour more accounts than necessary carrying privileged credentials." },
|
||||||
|
sources: [T1078_002, PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-asrep-roastable', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Fehlende Vorauthentifizierung begünstigt, dass Antworten ohne Anmeldedaten angefordert und offline geknackt werden.", en: "Missing pre-authentication favours responses being requested without credentials and cracked offline." },
|
||||||
|
sources: [mitre('T1558.004', 'Steal or Forge Kerberos Tickets: AS-REP Roasting'), PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-user-spn', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Benutzerkonten mit SPN begünstigen, dass Service-Tickets für ihr Kennwort angefordert und offline geknackt werden.", en: "User accounts with an SPN favour service tickets for their password being requested and cracked offline." },
|
||||||
|
sources: [T1558_003, PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-passwords-old', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Alte Kennwörter begünstigen, dass einmal erlangte oder geknackte Kennwörter weiter gültig sind.", en: "Old passwords favour once obtained or cracked passwords remaining valid." },
|
||||||
|
sources: [T1110_002, PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-password-not-required', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Konten ohne Kennwortpflicht begünstigen eine Anmeldung mit leerem oder trivialem Kennwort.", en: "Accounts without a password requirement favour signing in with an empty or trivial password." },
|
||||||
|
sources: [T1110_001, PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-recoverable-passwords', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Umkehrbar oder schwach gespeicherte Kennwörter begünstigen, dass sie ausgelesen oder schnell geknackt werden.", en: "Recoverably or weakly stored passwords favour them being read or cracked quickly." },
|
||||||
|
sources: [mitre('T1552.006', 'Unsecured Credentials: Group Policy Preferences'), T1110_002, PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-obsolete-components', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Veraltete Komponenten begünstigen, dass bekannte Schwachstellen und fehlende Schutzfunktionen bestehen bleiben.", en: "Obsolete components favour known vulnerabilities and missing protections persisting." },
|
||||||
|
sources: [T1210, PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-constrained-delegation-risky', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Konfigurierte Delegation begünstigt, dass ein übernommenes Konto sich gegenüber Zieldiensten als andere Nutzer ausgibt.", en: "Configured delegation favours a compromised account impersonating other users towards target services." },
|
||||||
|
sources: [KCD_OVERVIEW, PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'dc-health-inconsistent', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Inkonsistente Domain Controller begünstigen eine unvollständige Replikation und Wiederherstellung.", en: "Inconsistent domain controllers favour incomplete replication and recovery." },
|
||||||
|
sources: [PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-trust-configuration-weak', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Schwach konfigurierte Vertrauensstellungen begünstigen, dass eine Kompromittierung der vertrauten Domäne diese Domäne erfasst und die Wiederherstellung erschwert.", en: "Weakly configured trusts favour a compromise of the trusted domain reaching this domain and complicating recovery." },
|
||||||
|
sources: [mitre('T1134.005', 'Access Token Manipulation: SID-History Injection'), PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-explicit-certificate-mapping', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Explizite Zertifikatszuordnungen begünstigen, dass ein fremdes Zertifikat zur Anmeldung als dieses Konto dient.", en: "Explicit certificate mappings favour a foreign certificate serving to sign in as that account." },
|
||||||
|
sources: [T1649, PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-dangerous-execution-paths', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Beschreibbare Ziele in GPOs begünstigen, dass fremder Code bei der Anmeldung auf vielen Hosts ausgeführt wird.", en: "Writable targets in GPOs favour foreign code running on many hosts at sign-in." },
|
||||||
|
sources: [mitre('T1484.001', 'Domain or Tenant Policy Modification: Group Policy Modification'), PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'weak-certificate-cipher', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Schwache Zertifikatsverfahren begünstigen gefälschte oder abgefangene gesicherte Verbindungen.", en: "Weak certificate ciphers favour forged or intercepted secured connections." },
|
||||||
|
sources: [T1557, PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'entra-app-creation-unrestricted', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Uneingeschränkte Anwendungsregistrierung begünstigt dauerhafte Zugänge über eigene Anwendungen und deren Anmeldedaten.", en: "Unrestricted application registration favours lasting access through own applications and their credentials." },
|
||||||
|
sources: [T1098_001, SCUBA_AAD]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'entra-pim-controls-weak', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Fehlende PIM-Kontrollen begünstigen, dass privilegierte Rollen unbemerkt vergeben oder aktiviert werden.", en: "Missing PIM controls favour privileged roles being assigned or activated unnoticed." },
|
||||||
|
sources: [T1078_004, SCUBA_AAD]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'entra-password-expiry-enabled', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Erzwungener Kennwortwechsel begünstigt vorhersehbare, leichter zu erratende Kennwörter.", en: "Forced password changes favour predictable passwords that are easier to guess." },
|
||||||
|
sources: [SCUBA_AAD, CIS_M365]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'cis-password-policy-unmet', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Nicht erfüllte Kennwort- und Sperrempfehlungen begünstigen das Erraten und Knacken von Kennwörtern.", en: "Unmet password and lockout recommendations favour passwords being guessed and cracked." },
|
||||||
|
sources: [CIS_WINDOWS_SERVER, T1110_001]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'cis-anonymous-access-unmet', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Nicht erfüllte Empfehlungen zum anonymen Zugriff begünstigen das Auskundschaften von Konten und Freigaben ohne Anmeldung.", en: "Unmet anonymous access recommendations favour accounts and shares being explored without signing in." },
|
||||||
|
sources: [CIS_WINDOWS_SERVER, T1087_002]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'cis-local-privilege-hardening-unmet', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Nicht erfüllte UAC- und Systemobjekt-Empfehlungen begünstigen eine Rechteausweitung auf dem Host.", en: "Unmet UAC and system object recommendations favour privilege escalation on the host." },
|
||||||
|
sources: [CIS_WINDOWS_SERVER, T1548_002]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-powershell-logging-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichende PowerShell-Protokollierung begünstigt, dass Skriptaktivität nicht aufgezeichnet wird.", en: "Deviating PowerShell logging favours script activity not being recorded." },
|
||||||
|
sources: [T1059, CIS_WINDOWS_SERVER]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-event-logging-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichende Protokolleinstellungen begünstigen, dass Ereignisse fehlen oder überschrieben werden.", en: "Deviating log settings favour events missing or being overwritten." },
|
||||||
|
sources: [CIS_WINDOWS_SERVER]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-remote-desktop-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichende Remotedesktop-Einstellungen begünstigen die Ausbreitung über RDP.", en: "Deviating Remote Desktop settings favour spreading over RDP." },
|
||||||
|
sources: [T1021_001]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-remote-management-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichende WinRM-Einstellungen begünstigen die Ausbreitung über Windows-Remoteverwaltung.", en: "Deviating WinRM settings favour spreading over Windows Remote Management." },
|
||||||
|
sources: [T1021_006]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-smb-hardening-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichende SMB-Härtung begünstigt das Abfangen und Weiterleiten von SMB-Anmeldungen.", en: "Deviating SMB hardening favours SMB authentications being intercepted and relayed." },
|
||||||
|
sources: [T1557_001, T1021_002]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-secure-channel-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Ein abweichend geschützter sicherer Kanal begünstigt das Abfangen oder Manipulieren von Domänenkommunikation.", en: "A deviatingly protected secure channel favours domain communication being intercepted or tampered with." },
|
||||||
|
sources: [T1557]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-ntlm-hardening-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichende NTLM-Sitzungssicherheit begünstigt das Abfangen und Weiterleiten von NTLM-Anmeldungen.", en: "Deviating NTLM session security favours NTLM authentications being intercepted and relayed." },
|
||||||
|
sources: [T1557_001]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-anonymous-access-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichende Einschränkungen anonymen Zugriffs begünstigen das Auskundschaften ohne Anmeldung.", en: "Deviating restrictions on anonymous access favour exploration without signing in." },
|
||||||
|
sources: [T1087_002]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-lm-hash-storage-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichend gespeicherte LM-Hashes begünstigen, dass Kennwörter schnell geknackt werden.", en: "Deviatingly stored LM hashes favour passwords being cracked quickly." },
|
||||||
|
sources: [T1110_002]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-kerberos-encryption-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichende Kerberos-Verschlüsselungstypen begünstigen, dass Tickets mit schwachen Verfahren offline geknackt werden.", en: "Deviating Kerberos encryption types favour tickets with weak ciphers being cracked offline." },
|
||||||
|
sources: [T1558_003]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-local-privilege-hardening-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichende UAC- und Installationseinstellungen begünstigen eine Rechteausweitung auf dem Host.", en: "Deviating UAC and installation settings favour privilege escalation on the host." },
|
||||||
|
sources: [T1548_002]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-credential-protection-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichender Schutz von Anmeldedaten begünstigt, dass sie aus dem Speicher oder Cache eines Hosts gewonnen werden.", en: "Deviating credential protection favours credentials being recovered from a host's memory or cache." },
|
||||||
|
sources: [T1003, REMOTE_CREDENTIAL_GUARD]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-print-hardening-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichende Druckerhärtung begünstigt, dass der Druckdienst für erzwungene Anmeldungen oder Treiber-Rechteausweitung genutzt wird.", en: "Deviating printer hardening favours the print service being used for coerced authentication or driver-based privilege escalation." },
|
||||||
|
sources: [T1187, T1068]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-malware-protection-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichender Schadcodeschutz begünstigt, dass schädliche Dateien und Inhalte ausgeführt werden.", en: "Deviating malware protection favours malicious files and content being run." },
|
||||||
|
sources: [T1204_002]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-office-hardening-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichende Office-Härtung begünstigt, dass schädliche Dokumente Code ausführen.", en: "Deviating Office hardening favours malicious documents running code." },
|
||||||
|
sources: [T1204_002, T1566]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'gpo-local-device-access-deviates', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Abweichender lokaler Geräteschutz begünstigt den Missbrauch physischen Zugangs zu Hosts.", en: "Deviating local device protection favours misuse of physical access to hosts." },
|
||||||
|
sources: [T1200]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-tombstone-reanimation-right', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Das Recht, gelöschte Objekte wiederherzustellen, begünstigt die Rückkehr gelöschter privilegierter Konten.", en: "The right to restore deleted objects favours deleted privileged accounts returning." },
|
||||||
|
sources: [T1098, PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'ad-sdprop-protection-inconsistent', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Ein uneinheitlicher SDProp-Schutz begünstigt, dass abweichende Rechte an privilegierten Objekten bestehen bleiben.", en: "Inconsistent SDProp protection favours deviating rights on privileged objects persisting." },
|
||||||
|
sources: [T1098, PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'dcom-perflog-groups-populated', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Mitglieder dieser Gruppen begünstigen die Ausführung über DCOM auf entfernten Hosts.", en: "Members of these groups favour execution over DCOM on remote hosts." },
|
||||||
|
sources: [mitre('T1021.003', 'Remote Services: Distributed Component Object Model'), PURPLE_KNIGHT]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionId: 'entra-ca-groups-unprotected', role: 'prerequisite', techniqueIds: [],
|
||||||
|
statement: { de: "Ungeschützte Gruppen in Conditional Access begünstigen, dass Richtlinienausnahmen über Gruppenmitgliedschaft entstehen.", en: "Unprotected groups in Conditional Access favour policy exclusions arising through group membership." },
|
||||||
|
sources: [maesterTest('MT.1035')]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
conditionId: 'uncatalogued-deviations-gpo', role: 'prerequisite', techniqueIds: [],
|
conditionId: 'uncatalogued-deviations-gpo', role: 'prerequisite', techniqueIds: [],
|
||||||
statement: { de: "Eine noch nicht zugeordnete GPO-Abweichung begünstigt ein Risiko, das hier sichtbar bleibt, bis es katalogisiert ist.", en: "A GPO deviation that is not yet assigned favours a risk that stays visible here until it is catalogued." },
|
statement: { de: "Eine noch nicht zugeordnete GPO-Abweichung begünstigt ein Risiko, das hier sichtbar bleibt, bis es katalogisiert ist.", en: "A GPO deviation that is not yet assigned favours a risk that stays visible here until it is catalogued." },
|
||||||
|
|
@ -1581,7 +1875,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
'mdns-not-disabled', 'netbios-not-disabled', 'wpad-not-disabled',
|
'mdns-not-disabled', 'netbios-not-disabled', 'wpad-not-disabled',
|
||||||
'hardened-unc-paths-weakened', 'hardened-unc-paths-not-configured', 'smb-insecure-guest-logons', 'dns-nonsecure-dynamic-updates'
|
'hardened-unc-paths-weakened', 'hardened-unc-paths-not-configured', 'smb-insecure-guest-logons', 'dns-nonsecure-dynamic-updates'
|
||||||
,
|
,
|
||||||
'lm-auth-level-below-refuse-lm-ntlm', 'ldap-signing-enforcement-off', 'adidns-authenticated-create', 'dc-ldaps-legacy-tls', 'weak-trusted-root-certificate', 'cis-smb-hardening-unmet', 'cis-name-resolution-unmet', 'cis-authentication-hardening-unmet', 'cis-print-hardening-unmet', 'gpo-dns-client-policy-deviates'],
|
'lm-auth-level-below-refuse-lm-ntlm', 'ldap-signing-enforcement-off', 'adidns-authenticated-create', 'dc-ldaps-legacy-tls', 'weak-trusted-root-certificate', 'cis-smb-hardening-unmet', 'cis-name-resolution-unmet', 'cis-authentication-hardening-unmet', 'cis-print-hardening-unmet', 'gpo-dns-client-policy-deviates',
|
||||||
|
'weak-certificate-cipher', 'gpo-smb-hardening-deviates', 'gpo-secure-channel-deviates', 'gpo-ntlm-hardening-deviates', 'gpo-print-hardening-deviates'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [T1557_001, T1187]
|
sources: [T1557_001, T1187]
|
||||||
},
|
},
|
||||||
|
|
@ -1595,7 +1890,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
conditionIds: ['entra-permanent-privileged-assignments', 'entra-external-user-privileged-role', 'entra-client-secret-privileged-role',
|
conditionIds: ['entra-permanent-privileged-assignments', 'entra-external-user-privileged-role', 'entra-client-secret-privileged-role',
|
||||||
'entra-admins-not-cloud-only', 'entra-privileged-mfa-not-phishing-resistant', 'entra-admin-mfa-not-required',
|
'entra-admins-not-cloud-only', 'entra-privileged-mfa-not-phishing-resistant', 'entra-admin-mfa-not-required',
|
||||||
'entra-global-admin-overused', 'entra-privileged-user-mailbox', 'entra-admin-managed-device-not-required', 'azure-root-permanent-access',
|
'entra-global-admin-overused', 'entra-privileged-user-mailbox', 'entra-admin-managed-device-not-required', 'azure-root-permanent-access',
|
||||||
'entra-global-admin-count-outside-range', 'entra-emergency-access-not-excluded', 'entra-eligible-roles-unused', 'entra-privileged-unusual-signin', 'entra-privileged-accounts-inactive', 'azure-management-group-creation-open'],
|
'entra-global-admin-count-outside-range', 'entra-emergency-access-not-excluded', 'entra-eligible-roles-unused', 'entra-privileged-unusual-signin', 'entra-privileged-accounts-inactive', 'azure-management-group-creation-open',
|
||||||
|
'entra-pim-controls-weak', 'entra-ca-groups-unprotected'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [SCUBA_AAD, T1078_004]
|
sources: [SCUBA_AAD, T1078_004]
|
||||||
},
|
},
|
||||||
|
|
@ -1609,7 +1905,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
conditionIds: ['entra-legacy-auth-not-blocked', 'entra-high-risk-signins-not-blocked', 'entra-signin-risk-not-covered',
|
conditionIds: ['entra-legacy-auth-not-blocked', 'entra-high-risk-signins-not-blocked', 'entra-signin-risk-not-covered',
|
||||||
'entra-mfa-not-phishing-resistant', 'entra-weak-mfa-methods-enabled', 'entra-password-protection-not-enforced', 'entra-smart-lockout-weak',
|
'entra-mfa-not-phishing-resistant', 'entra-weak-mfa-methods-enabled', 'entra-password-protection-not-enforced', 'entra-smart-lockout-weak',
|
||||||
'entra-user-risk-not-covered', 'entra-mfa-registration-incomplete', 'entra-managed-device-not-required', 'entra-mfa-registration-unrestricted', 'entra-mfa-fatigue-protection-weak', 'entra-unmanaged-sessions-persistent',
|
'entra-user-risk-not-covered', 'entra-mfa-registration-incomplete', 'entra-managed-device-not-required', 'entra-mfa-registration-unrestricted', 'entra-mfa-fatigue-protection-weak', 'entra-unmanaged-sessions-persistent',
|
||||||
'entra-ca-policy-integrity-gaps', 'entra-ca-coverage-gaps', 'entra-fido2-restrictions-off', 'entra-authenticator-otp-allowed', 'intune-device-compliance-gaps'],
|
'entra-ca-policy-integrity-gaps', 'entra-ca-coverage-gaps', 'entra-fido2-restrictions-off', 'entra-authenticator-otp-allowed', 'intune-device-compliance-gaps',
|
||||||
|
'entra-password-expiry-enabled'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [SCUBA_AAD, T1110_003, T1078_004]
|
sources: [SCUBA_AAD, T1110_003, T1078_004]
|
||||||
},
|
},
|
||||||
|
|
@ -1622,7 +1919,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
},
|
},
|
||||||
conditionIds: ['ad-admins-delegatable', 'ad-admins-not-protected-users', 'ad-builtin-admin-used', 'ad-privileged-spn', 'ad-shadow-credentials-privileged',
|
conditionIds: ['ad-admins-delegatable', 'ad-admins-not-protected-users', 'ad-builtin-admin-used', 'ad-privileged-spn', 'ad-shadow-credentials-privileged',
|
||||||
'dnsadmins-unprivileged-members', 'schema-admins-not-empty',
|
'dnsadmins-unprivileged-members', 'schema-admins-not-empty',
|
||||||
'dsheuristics-ldap-mitigation-off', 'cis-domain-controller-options-unmet'],
|
'dsheuristics-ldap-mitigation-off', 'cis-domain-controller-options-unmet',
|
||||||
|
'ad-dangerous-acl-delegation', 'rodc-credential-caching-risky', 'gpo-link-delegation-broad', 'ad-privileged-membership-hygiene'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [PROTECTED_USERS, T1078_002, T1558_003]
|
sources: [PROTECTED_USERS, T1078_002, T1558_003]
|
||||||
},
|
},
|
||||||
|
|
@ -1634,7 +1932,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
en: 'Several observed findings favour the same path: abusing Kerberos delegation to impersonate other users. This is not proof that an attack is possible or took place.'
|
en: 'Several observed findings favour the same path: abusing Kerberos delegation to impersonate other users. This is not proof that an attack is possible or took place.'
|
||||||
},
|
},
|
||||||
conditionIds: ['ad-rbcd-write-on-dc', 'ad-rbcd-write-on-krbtgt', 'ad-rbcd-on-dc-or-krbtgt', 'ad-delegation-to-dc-or-krbtgt',
|
conditionIds: ['ad-rbcd-write-on-dc', 'ad-rbcd-write-on-krbtgt', 'ad-rbcd-on-dc-or-krbtgt', 'ad-delegation-to-dc-or-krbtgt',
|
||||||
'ad-unconstrained-delegation', 'ad-rbcd-computer-takeover', 'ad-admins-delegatable', 'ad-users-can-add-computers', 'gpo-delegation-right-deviates'],
|
'ad-unconstrained-delegation', 'ad-rbcd-computer-takeover', 'ad-admins-delegatable', 'ad-users-can-add-computers', 'gpo-delegation-right-deviates',
|
||||||
|
'ad-constrained-delegation-risky'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [KCD_OVERVIEW]
|
sources: [KCD_OVERVIEW]
|
||||||
},
|
},
|
||||||
|
|
@ -1645,7 +1944,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
de: 'Mehrere beobachtete Befunde begünstigen denselben Weg: über Active Directory Certificate Services Zertifikate im Namen anderer Konten zu erhalten. Das ist kein Nachweis, dass ein Angriff möglich ist oder stattfand.',
|
de: 'Mehrere beobachtete Befunde begünstigen denselben Weg: über Active Directory Certificate Services Zertifikate im Namen anderer Konten zu erhalten. Das ist kein Nachweis, dass ein Angriff möglich ist oder stattfand.',
|
||||||
en: 'Several observed findings favour the same path: obtaining certificates in the name of other accounts through Active Directory Certificate Services. This is not proof that an attack is possible or took place.'
|
en: 'Several observed findings favour the same path: obtaining certificates in the name of other accounts through Active Directory Certificate Services. This is not proof that an attack is possible or took place.'
|
||||||
},
|
},
|
||||||
conditionIds: ['adcs-template-requester-san', 'adcs-template-insecure-config', 'adcs-dangerous-control-paths', 'adcs-web-enrollment-esc8'],
|
conditionIds: ['adcs-template-requester-san', 'adcs-template-insecure-config', 'adcs-dangerous-control-paths', 'adcs-web-enrollment-esc8',
|
||||||
|
'ad-explicit-certificate-mapping'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [T1649]
|
sources: [T1649]
|
||||||
},
|
},
|
||||||
|
|
@ -1658,7 +1958,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
},
|
},
|
||||||
conditionIds: ['ad-privileged-spn', 'ad-spn-without-aes', 'ad-rc4-des-on-dcs', 'ad-privileged-password-never-expires', 'ad-password-never-expires',
|
conditionIds: ['ad-privileged-spn', 'ad-spn-without-aes', 'ad-rc4-des-on-dcs', 'ad-privileged-password-never-expires', 'ad-password-never-expires',
|
||||||
'ad-service-accounts-no-strong-policy', 'kerberos-rc4-des-allowed',
|
'ad-service-accounts-no-strong-policy', 'kerberos-rc4-des-allowed',
|
||||||
'domain-admins-password-never-expires', 'gmsa-not-used'],
|
'domain-admins-password-never-expires', 'gmsa-not-used',
|
||||||
|
'ad-asrep-roastable', 'ad-user-spn', 'gpo-kerberos-encryption-deviates'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [T1558_003]
|
sources: [T1558_003]
|
||||||
},
|
},
|
||||||
|
|
@ -1671,7 +1972,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
},
|
},
|
||||||
conditionIds: ['rdp-without-nla', 'winrm-basic-auth', 'winrm-unencrypted', 'winrm-remote-management-allowed', 'smbv1-enabled', 'host-firewall-off', 'host-firewall-inbound-allowed', 'network-logon-rights-broad',
|
conditionIds: ['rdp-without-nla', 'winrm-basic-auth', 'winrm-unencrypted', 'winrm-remote-management-allowed', 'smbv1-enabled', 'host-firewall-off', 'host-firewall-inbound-allowed', 'network-logon-rights-broad',
|
||||||
'laps-not-deployed', 'ad-smartcard-password-rotation-disabled', 'rdp-sessions-not-timed-out', 'terminal-services-gpo-incomplete', 'rdp-connection-security-weak',
|
'laps-not-deployed', 'ad-smartcard-password-rotation-disabled', 'rdp-sessions-not-timed-out', 'terminal-services-gpo-incomplete', 'rdp-connection-security-weak',
|
||||||
'cis-remote-desktop-unmet', 'cis-remote-management-unmet', 'cis-firewall-profile-unmet', 'cis-network-stack-unmet', 'gpo-network-logon-right-deviates', 'gpo-firewall-profile-deviates'],
|
'cis-remote-desktop-unmet', 'cis-remote-management-unmet', 'cis-firewall-profile-unmet', 'cis-network-stack-unmet', 'gpo-network-logon-right-deviates', 'gpo-firewall-profile-deviates',
|
||||||
|
'gpo-remote-desktop-deviates', 'gpo-remote-management-deviates', 'dcom-perflog-groups-populated'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [T1021]
|
sources: [T1021]
|
||||||
},
|
},
|
||||||
|
|
@ -1684,7 +1986,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
},
|
},
|
||||||
conditionIds: ['network-credentials-stored', 'lsass-custom-ssp-allowed', 'winrm-runas-credentials-stored', 'uac-weak',
|
conditionIds: ['network-credentials-stored', 'lsass-custom-ssp-allowed', 'winrm-runas-credentials-stored', 'uac-weak',
|
||||||
'dangerous-user-rights-granted', 'rdp-passwords-saved', 'always-install-elevated', 'remote-credential-delegation-unprotected',
|
'dangerous-user-rights-granted', 'rdp-passwords-saved', 'always-install-elevated', 'remote-credential-delegation-unprotected',
|
||||||
'autoadminlogon-not-disabled', 'cis-print-hardening-unmet', 'cis-user-rights-unmet', 'cis-credential-handling-unmet', 'gpo-privileged-rights-deviate', 'gpo-office-forms-auth-hosts-deviate'],
|
'autoadminlogon-not-disabled', 'cis-print-hardening-unmet', 'cis-user-rights-unmet', 'cis-credential-handling-unmet', 'gpo-privileged-rights-deviate', 'gpo-office-forms-auth-hosts-deviate',
|
||||||
|
'cis-local-privilege-hardening-unmet', 'gpo-local-privilege-hardening-deviates', 'gpo-credential-protection-deviates'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [T1555_004, T1547_005, T1548_002]
|
sources: [T1555_004, T1547_005, T1548_002]
|
||||||
},
|
},
|
||||||
|
|
@ -1697,7 +2000,9 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
},
|
},
|
||||||
conditionIds: ['powershell-logging-off', 'dc-audit-incomplete',
|
conditionIds: ['powershell-logging-off', 'dc-audit-incomplete',
|
||||||
'host-audit-policy-incomplete', 'process-creation-not-logged', 'security-event-log-small', 'ntlm-usage-not-audited', 'firewall-logging-off', 'identity-threat-detection-missing', 'cloud-security-logs-not-forwarded', 'entra-privileged-role-alerts-missing',
|
'host-audit-policy-incomplete', 'process-creation-not-logged', 'security-event-log-small', 'ntlm-usage-not-audited', 'firewall-logging-off', 'identity-threat-detection-missing', 'cloud-security-logs-not-forwarded', 'entra-privileged-role-alerts-missing',
|
||||||
'entra-leaked-credential-detection-limited', 'cis-audit-policy-unmet', 'cis-event-log-unmet', 'gpo-audit-policy-deviates', 'gpo-firewall-logging-deviates'],
|
'entra-leaked-credential-detection-limited', 'cis-audit-policy-unmet', 'cis-event-log-unmet', 'gpo-audit-policy-deviates', 'gpo-firewall-logging-deviates',
|
||||||
|
'ad-default-container-redirected',
|
||||||
|
'gpo-powershell-logging-deviates', 'gpo-event-logging-deviates'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [CIS_WINDOWS_SERVER, PINGCASTLE_RULES]
|
sources: [CIS_WINDOWS_SERVER, PINGCASTLE_RULES]
|
||||||
},
|
},
|
||||||
|
|
@ -1711,7 +2016,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
conditionIds: ['password-length-below-14', 'password-length-below-8', 'password-complexity-off', 'account-lockout-threshold-weak',
|
conditionIds: ['password-length-below-14', 'password-length-below-8', 'password-complexity-off', 'account-lockout-threshold-weak',
|
||||||
'account-lockout-duration-short', 'ad-privileged-weak-password-policy', 'ad-service-accounts-no-strong-policy',
|
'account-lockout-duration-short', 'ad-privileged-weak-password-policy', 'ad-service-accounts-no-strong-policy',
|
||||||
'ad-password-never-expires', 'ad-privileged-password-never-expires', 'kerberos-armoring-off',
|
'ad-password-never-expires', 'ad-privileged-password-never-expires', 'kerberos-armoring-off',
|
||||||
'domain-admins-password-never-expires', 'cis-builtin-accounts-unmet', 'cis-account-database-unmet', 'gpo-lockout-timing-deviates', 'gpo-password-lifetime-deviates', 'gpo-reversible-password-storage-deviates'],
|
'domain-admins-password-never-expires', 'cis-builtin-accounts-unmet', 'cis-account-database-unmet', 'gpo-lockout-timing-deviates', 'gpo-password-lifetime-deviates', 'gpo-reversible-password-storage-deviates',
|
||||||
|
'ad-passwords-old', 'ad-password-not-required', 'ad-recoverable-passwords', 'cis-password-policy-unmet', 'gpo-lm-hash-storage-deviates'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [T1110_001, T1110_002]
|
sources: [T1110_001, T1110_002]
|
||||||
},
|
},
|
||||||
|
|
@ -1723,7 +2029,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
en: "Several declared settings or observed findings favour the same path: running malicious files, scripts or removable media on hosts. This is not proof that an attack is possible or took place."
|
en: "Several declared settings or observed findings favour the same path: running malicious files, scripts or removable media on hosts. This is not proof that an attack is possible or took place."
|
||||||
},
|
},
|
||||||
conditionIds: ['defender-asr-not-enforced', 'defender-protection-weakened', 'web-protection-off', 'mark-of-the-web-suppressed', 'autorun-enabled', 'script-files-run-on-double-click', 'script-engines-reach-internet',
|
conditionIds: ['defender-asr-not-enforced', 'defender-protection-weakened', 'web-protection-off', 'mark-of-the-web-suppressed', 'autorun-enabled', 'script-files-run-on-double-click', 'script-engines-reach-internet',
|
||||||
'cis-defender-unmet', 'cis-software-installation-unmet', 'cis-exploit-mitigations-unmet', 'gpo-application-control-deviates'],
|
'cis-defender-unmet', 'cis-software-installation-unmet', 'cis-exploit-mitigations-unmet', 'gpo-application-control-deviates',
|
||||||
|
'gpo-dangerous-execution-paths', 'gpo-malware-protection-deviates', 'gpo-office-hardening-deviates'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [T1204_002, T1059]
|
sources: [T1204_002, T1059]
|
||||||
},
|
},
|
||||||
|
|
@ -1734,7 +2041,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
de: "Mehrere deklarierte Einstellungen oder beobachtete Befunde begünstigen denselben Weg: dauerhafte, unbemerkte Rechte an geschützten AD-Konten zu behalten. Das ist kein Nachweis, dass ein Angriff möglich ist oder stattfand.",
|
de: "Mehrere deklarierte Einstellungen oder beobachtete Befunde begünstigen denselben Weg: dauerhafte, unbemerkte Rechte an geschützten AD-Konten zu behalten. Das ist kein Nachweis, dass ein Angriff möglich ist oder stattfand.",
|
||||||
en: "Several declared settings or observed findings favour the same path: keeping lasting, unnoticed rights on protected AD accounts. This is not proof that an attack is possible or took place."
|
en: "Several declared settings or observed findings favour the same path: keeping lasting, unnoticed rights on protected AD accounts. This is not proof that an attack is possible or took place."
|
||||||
},
|
},
|
||||||
conditionIds: ['adminsdholder-permissions-changed', 'ad-stale-admincount'],
|
conditionIds: ['adminsdholder-permissions-changed', 'ad-stale-admincount',
|
||||||
|
'ad-dcshadow-evidence', 'ad-primary-group-anomaly', 'krbtgt-password-old', 'ad-sidhistory-abuse', 'ad-recent-sensitive-changes', 'ad-tombstone-reanimation-right', 'ad-sdprop-protection-inconsistent'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [T1098]
|
sources: [T1098]
|
||||||
},
|
},
|
||||||
|
|
@ -1745,7 +2053,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
de: "Mehrere deklarierte Einstellungen oder beobachtete Befunde begünstigen denselben Weg: Active Directory ohne oder mit geringen Rechten zu erkunden. Das ist kein Nachweis, dass ein Angriff möglich ist oder stattfand.",
|
de: "Mehrere deklarierte Einstellungen oder beobachtete Befunde begünstigen denselben Weg: Active Directory ohne oder mit geringen Rechten zu erkunden. Das ist kein Nachweis, dass ein Angriff möglich ist oder stattfand.",
|
||||||
en: "Several declared settings or observed findings favour the same path: exploring Active Directory without or with minimal rights. This is not proof that an attack is possible or took place."
|
en: "Several declared settings or observed findings favour the same path: exploring Active Directory without or with minimal rights. This is not proof that an attack is possible or took place."
|
||||||
},
|
},
|
||||||
conditionIds: ['anonymous-ldap-bind', 'anonymous-directory-access', 'anonymous-nspi-access', 'prewin2000-access-broad', 'prewin2000-membership-changed', 'net-session-enumeration-open', 'anonymous-named-pipes', 'guest-account-enabled'],
|
conditionIds: ['anonymous-ldap-bind', 'anonymous-directory-access', 'anonymous-nspi-access', 'prewin2000-access-broad', 'prewin2000-membership-changed', 'net-session-enumeration-open', 'anonymous-named-pipes', 'guest-account-enabled',
|
||||||
|
'cis-anonymous-access-unmet', 'gpo-anonymous-access-deviates'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [T1087_002]
|
sources: [T1087_002]
|
||||||
},
|
},
|
||||||
|
|
@ -1757,7 +2066,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
en: "Several declared settings or observed findings favour the same path: a long or incomplete recovery after deletion or destruction. This is not proof that an attack is possible or took place."
|
en: "Several declared settings or observed findings favour the same path: a long or incomplete recovery after deletion or destruction. This is not proof that an attack is possible or took place."
|
||||||
},
|
},
|
||||||
conditionIds: ['ad-backup-outdated', 'ad-recycle-bin-disabled', 'ou-deletion-unprotected', 'dc-redundancy-missing',
|
conditionIds: ['ad-backup-outdated', 'ad-recycle-bin-disabled', 'ou-deletion-unprotected', 'dc-redundancy-missing',
|
||||||
'dc-subnet-declaration-incomplete'],
|
'dc-subnet-declaration-incomplete',
|
||||||
|
'dc-health-inconsistent', 'ad-trust-configuration-weak'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [T1490, T1485]
|
sources: [T1490, T1485]
|
||||||
},
|
},
|
||||||
|
|
@ -1769,7 +2079,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
en: "Several declared settings or observed findings favour the same path: gaining lasting access to tenant data through consented or forgotten applications. This is not proof that an attack is possible or took place."
|
en: "Several declared settings or observed findings favour the same path: gaining lasting access to tenant data through consented or forgotten applications. This is not proof that an attack is possible or took place."
|
||||||
},
|
},
|
||||||
conditionIds: ['entra-users-can-consent-apps', 'entra-users-consent-risky-apps', 'entra-app-credentials-stale',
|
conditionIds: ['entra-users-can-consent-apps', 'entra-users-consent-risky-apps', 'entra-app-credentials-stale',
|
||||||
'entra-admin-consent-workflow-missing', 'entra-app-credentials-expiring'],
|
'entra-admin-consent-workflow-missing', 'entra-app-credentials-expiring',
|
||||||
|
'entra-app-creation-unrestricted'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [T1528, T1098_001]
|
sources: [T1528, T1098_001]
|
||||||
},
|
},
|
||||||
|
|
@ -1803,7 +2114,9 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
de: "Mehrere deklarierte Einstellungen oder beobachtete Befunde begünstigen denselben Weg: lokalen oder physischen Zugang zu Hosts zu missbrauchen. Das ist kein Nachweis, dass ein Angriff möglich ist oder stattfand.",
|
de: "Mehrere deklarierte Einstellungen oder beobachtete Befunde begünstigen denselben Weg: lokalen oder physischen Zugang zu Hosts zu missbrauchen. Das ist kein Nachweis, dass ein Angriff möglich ist oder stattfand.",
|
||||||
en: "Several declared settings or observed findings favour the same path: misusing local or physical access to hosts. This is not proof that an attack is possible or took place."
|
en: "Several declared settings or observed findings favour the same path: misusing local or physical access to hosts. This is not proof that an attack is possible or took place."
|
||||||
},
|
},
|
||||||
conditionIds: ['cis-sign-in-screen-unmet', 'cis-device-access-unmet', 'gpo-local-logon-right-deviates', 'gpo-logon-hours-enforcement-deviates'],
|
conditionIds: ['cis-sign-in-screen-unmet', 'cis-device-access-unmet', 'gpo-local-logon-right-deviates', 'gpo-logon-hours-enforcement-deviates',
|
||||||
|
'gpo-convenience-pin-sign-in-deviates',
|
||||||
|
'gpo-local-device-access-deviates'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [T1200, CIS_WINDOWS_SERVER]
|
sources: [T1200, CIS_WINDOWS_SERVER]
|
||||||
},
|
},
|
||||||
|
|
@ -1814,7 +2127,8 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
de: "Mehrere deklarierte Einstellungen oder beobachtete Befunde begünstigen denselben Weg: bekannte Schwachstellen und abweichende Einstellungen länger bestehen zu lassen. Das ist kein Nachweis, dass ein Angriff möglich ist oder stattfand.",
|
de: "Mehrere deklarierte Einstellungen oder beobachtete Befunde begünstigen denselben Weg: bekannte Schwachstellen und abweichende Einstellungen länger bestehen zu lassen. Das ist kein Nachweis, dass ein Angriff möglich ist oder stattfand.",
|
||||||
en: "Several declared settings or observed findings favour the same path: known vulnerabilities and drifted settings persisting longer. This is not proof that an attack is possible or took place."
|
en: "Several declared settings or observed findings favour the same path: known vulnerabilities and drifted settings persisting longer. This is not proof that an attack is possible or took place."
|
||||||
},
|
},
|
||||||
conditionIds: ['cis-windows-update-unmet', 'cis-policy-processing-unmet', 'cis-time-sync-unmet'],
|
conditionIds: ['cis-windows-update-unmet', 'cis-policy-processing-unmet', 'cis-time-sync-unmet',
|
||||||
|
'ad-obsolete-components'],
|
||||||
minimumSatisfied: 2,
|
minimumSatisfied: 2,
|
||||||
sources: [T1210, CIS_WINDOWS_SERVER]
|
sources: [T1210, CIS_WINDOWS_SERVER]
|
||||||
},
|
},
|
||||||
|
|
@ -2062,6 +2376,237 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
ruleText: 'Policy where the password length is less than 8 characters', sources: [PINGCASTLE_RULES] },
|
ruleText: 'Policy where the password length is less than 8 characters', sources: [PINGCASTLE_RULES] },
|
||||||
{ tool: 'pingcastle', ruleId: 'S-PwdNeverExpires', conditionId: 'ad-password-never-expires', polarity: 'fail-means-met',
|
{ tool: 'pingcastle', ruleId: 'S-PwdNeverExpires', conditionId: 'ad-password-never-expires', polarity: 'fail-means-met',
|
||||||
ruleText: 'Accounts which have never expiring passwords', sources: [PINGCASTLE_RULES] },
|
ruleText: 'Accounts which have never expiring passwords', sources: [PINGCASTLE_RULES] },
|
||||||
|
{ tool: 'pingcastle', ruleId: 'S-DefaultOUChanged', conditionId: 'ad-default-container-redirected', polarity: 'fail-means-met',
|
||||||
|
ruleText: 'Check if default OU location has been changed within the domain.', sources: [PINGCASTLE_RULES] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000013', conditionId: 'ad-dcshadow-evidence', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Evidence of Mimikatz DCShadow attack", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000014', conditionId: 'ad-primary-group-anomaly', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Users and computers with non-default Primary Group IDs", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000078', conditionId: 'ad-primary-group-anomaly', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Users and computers without readable PGID", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000017', conditionId: 'krbtgt-password-old', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Kerberos KRBTGT account with old password", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000023', conditionId: 'ad-sidhistory-abuse', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Well-known privileged SIDs in SIDHistory", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000060', conditionId: 'ad-sidhistory-abuse', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Recent SIDHistory changes on objects", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000033', conditionId: 'ad-recent-sensitive-changes', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Recent privileged account creation activity", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000043', conditionId: 'ad-recent-sensitive-changes', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Changes to privileged group membership in the last 7 days", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000047', conditionId: 'ad-recent-sensitive-changes', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Changes to default security descriptor schema in the last 90 days", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000048', conditionId: 'ad-recent-sensitive-changes', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Changes to Default Domain Policy or Default Domain Controllers Policy in the last 7 days", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000082', conditionId: 'ad-recent-sensitive-changes', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Changes to AD Display Specifiers in the past 90 days", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000101', conditionId: 'ad-recent-sensitive-changes', polarity: 'fail-means-met',
|
||||||
|
ruleText: "SYSVOL Executable Changes", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000021', conditionId: 'ad-dangerous-acl-delegation', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Non-default principals with DC Sync rights on the domain", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000025', conditionId: 'ad-dangerous-acl-delegation', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Privileged objects with unprivileged owners", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000050', conditionId: 'ad-dangerous-acl-delegation', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Domain Controller owner is not an administrator", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000056', conditionId: 'ad-dangerous-acl-delegation', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Enterprise Key Admins with full access to domain", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000081', conditionId: 'ad-dangerous-acl-delegation', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Non-standard schema permissions", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000083', conditionId: 'ad-dangerous-acl-delegation', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Non-privileged users with access to gMSA passwords", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000089', conditionId: 'ad-dangerous-acl-delegation', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Users with permissions to set Server Trust Account", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000092', conditionId: 'ad-dangerous-acl-delegation', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Non-default access to DPAPI key", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000185', conditionId: 'ad-tombstone-reanimation-right', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Objects with Reanimate-Tombstones extended right", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000193', conditionId: 'ad-dangerous-acl-delegation', polarity: 'fail-means-met',
|
||||||
|
ruleText: "OU permissions enabling BadSuccessor dMSA escalation", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000022', conditionId: 'rodc-credential-caching-risky', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Risky RODC credential caching", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000138', conditionId: 'rodc-credential-caching-risky', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Privileged user credentials cached on RODC", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000032', conditionId: 'gpo-link-delegation-broad', polarity: 'fail-means-met',
|
||||||
|
ruleText: "GPO linking delegation at the domain level", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000040', conditionId: 'gpo-link-delegation-broad', polarity: 'fail-means-met',
|
||||||
|
ruleText: "GPO linking delegation at the domain controller OU level", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000041', conditionId: 'gpo-link-delegation-broad', polarity: 'fail-means-met',
|
||||||
|
ruleText: "GPO linking delegation at the AD Site level", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000038', conditionId: 'ad-privileged-membership-hygiene', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Enabled admin accounts that are inactive", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000039', conditionId: 'ad-privileged-membership-hygiene', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Privileged users that are disabled", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000046', conditionId: 'ad-sdprop-protection-inconsistent', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Objects in privileged groups without adminCount=1 (SDProp)", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000053', conditionId: 'ad-sdprop-protection-inconsistent', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Operator groups no longer protected by AdminSDHolder and SDProp", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000061', conditionId: 'ad-privileged-membership-hygiene', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Computer Accounts in Privileged Groups", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000064', conditionId: 'ad-privileged-membership-hygiene', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Forest contains more than 50 privileged accounts", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000110', conditionId: 'ad-recent-sensitive-changes', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Ephemeral Admins", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000124', conditionId: 'ad-privileged-membership-hygiene', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Foreign Security Principals in Privileged Group", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000150', conditionId: 'ad-privileged-membership-hygiene', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Operators Groups that are not empty", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000355', conditionId: 'dcom-perflog-groups-populated', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Distributed COM Users group or Performance Log Users group are not empty", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000027', conditionId: 'ad-asrep-roastable', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Users with Kerberos pre-authentication disabled", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000042', conditionId: 'ad-user-spn', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Users with SPN defined", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000037', conditionId: 'ad-passwords-old', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Users with old passwords", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000049', conditionId: 'ad-passwords-old', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Built-in domain Administrator account with old password (180 days)", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000059', conditionId: 'ad-passwords-old', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Computers with password last set over 90 days ago", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000062', conditionId: 'ad-passwords-old', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Admins with old passwords", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000063', conditionId: 'ad-passwords-old', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Domain Controllers with old passwords", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000068', conditionId: 'ad-trust-configuration-weak', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Trust accounts with old passwords", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000072', conditionId: 'ad-passwords-old', polarity: 'fail-means-met',
|
||||||
|
ruleText: "gMSA objects with old passwords", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000236', conditionId: 'ad-passwords-old', polarity: 'fail-means-met',
|
||||||
|
ruleText: "User accounts using Smart Card authentication with old password", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000074', conditionId: 'ad-password-not-required', polarity: 'fail-means-met',
|
||||||
|
ruleText: "User accounts with password not required", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000015', conditionId: 'ad-recoverable-passwords', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Reversible passwords found in GPOs", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000076', conditionId: 'ad-recoverable-passwords', polarity: 'fail-means-met',
|
||||||
|
ruleText: "User accounts that store passwords with reversible encryption", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000188', conditionId: 'ad-recoverable-passwords', polarity: 'fail-means-met',
|
||||||
|
ruleText: "GPO Weak LM Hash storage enabled", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000339', conditionId: 'ad-recoverable-passwords', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Users with the attribute userPassword set", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000075', conditionId: 'kerberos-rc4-des-allowed', polarity: 'fail-means-met',
|
||||||
|
ruleText: "User accounts that use DES encryption", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000051', conditionId: 'ad-obsolete-components', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Domains with obsolete functional levels", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000054', conditionId: 'ad-obsolete-components', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Computers with older OS versions", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000097', conditionId: 'ad-obsolete-components', polarity: 'fail-means-met',
|
||||||
|
ruleText: "NTFRS SYSVOL Replication", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000057', conditionId: 'ad-constrained-delegation-risky', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Objects with constrained delegation configured", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000058', conditionId: 'ad-constrained-delegation-risky', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Kerberos protocol transition delegation configured", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000100', conditionId: 'ad-constrained-delegation-risky', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Accounts with Constrained Delegation configured to ghost SPN", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000069', conditionId: 'dc-health-inconsistent', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Domain Controllers in inconsistent state", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000073', conditionId: 'dc-health-inconsistent', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Domain Controllers that have not authenticated to the domain for more than 45 days", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000087', conditionId: 'ad-trust-configuration-weak', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Domain trust to a third-party domain without quarantine", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000088', conditionId: 'ad-trust-configuration-weak', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Outbound forest trust with SID History enabled", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000108', conditionId: 'ad-trust-configuration-weak', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Dangerous Trust Attribute Set", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000102', conditionId: 'ad-explicit-certificate-mapping', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Accounts with altSecurityIdentities configured", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000106', conditionId: 'adminsdholder-permissions-changed', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Inheritance enabled on AdminSDHolder object", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000314', conditionId: 'adcs-dangerous-control-paths', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Unexpected accounts in Cert Publishers Group", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000086', conditionId: 'weak-certificate-cipher', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Weak certificate cipher", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000189', conditionId: 'gpo-dangerous-execution-paths', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Writable shortcuts found in GPO", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'purple-knight', ruleId: 'SI000304', conditionId: 'gpo-dangerous-execution-paths', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Dangerous GPO logon script path", sources: [PURPLE_KNIGHT] },
|
||||||
|
{ tool: 'maester', ruleId: 'CIS.M365.1.3.1', conditionId: 'entra-password-expiry-enabled', polarity: 'fail-means-met',
|
||||||
|
ruleText: "(L1) Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)'", sources: [CIS_M365] },
|
||||||
|
{ tool: 'maester', ruleId: 'CISA.MS.AAD.6.1', conditionId: 'entra-password-expiry-enabled', polarity: 'fail-means-met',
|
||||||
|
ruleText: "User passwords SHALL NOT expire.", sources: [SCUBA_AAD] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1024.pwagePolicyNew', conditionId: 'entra-password-expiry-enabled', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Do not expire passwords.", sources: [maesterTest('MT.1024')] },
|
||||||
|
{ tool: 'maester', ruleId: 'CISA.MS.AAD.2.1', conditionId: 'entra-user-risk-not-covered', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Users detected as high risk SHALL be blocked.", sources: [SCUBA_AAD] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1013', conditionId: 'entra-user-risk-not-covered', polarity: 'fail-means-met',
|
||||||
|
ruleText: "At least one Conditional Access policy is configured to require new password when user risk is high.", sources: [maesterTest('MT.1013')] },
|
||||||
|
{ tool: 'maester', ruleId: 'CISA.MS.AAD.3.2', conditionId: 'entra-ca-coverage-gaps', polarity: 'fail-means-met',
|
||||||
|
ruleText: "If phishing-resistant MFA has not been enforced, an alternative MFA method SHALL be enforced for all users.", sources: [SCUBA_AAD] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1003', conditionId: 'entra-ca-coverage-gaps', polarity: 'fail-means-met',
|
||||||
|
ruleText: "At least one Conditional Access policy is configured with All Apps.", sources: [maesterTest('MT.1003')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1004', conditionId: 'entra-ca-coverage-gaps', polarity: 'fail-means-met',
|
||||||
|
ruleText: "At least one Conditional Access policy is configured with All Apps and All Users.", sources: [maesterTest('MT.1004')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1007', conditionId: 'entra-ca-coverage-gaps', polarity: 'fail-means-met',
|
||||||
|
ruleText: "At least one Conditional Access policy is configured to require MFA for all users.", sources: [maesterTest('MT.1007')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1008', conditionId: 'entra-ca-coverage-gaps', polarity: 'fail-means-met',
|
||||||
|
ruleText: "At least one Conditional Access policy is configured to require MFA for Azure management.", sources: [maesterTest('MT.1008')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1016', conditionId: 'entra-ca-coverage-gaps', polarity: 'fail-means-met',
|
||||||
|
ruleText: "At least one Conditional Access policy is configured to require MFA for guest access.", sources: [maesterTest('MT.1016')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1052', conditionId: 'entra-ca-coverage-gaps', polarity: 'fail-means-met',
|
||||||
|
ruleText: "At least one Conditional Access policy is targeting the Device Code authentication flow.", sources: [maesterTest('MT.1052')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1006', conditionId: 'entra-admin-mfa-not-required', polarity: 'fail-means-met',
|
||||||
|
ruleText: "At least one Conditional Access policy is configured to require MFA for admins.", sources: [maesterTest('MT.1006')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1001', conditionId: 'entra-managed-device-not-required', polarity: 'fail-means-met',
|
||||||
|
ruleText: "At least one Conditional Access policy is configured with device compliance.", sources: [maesterTest('MT.1001')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1035', conditionId: 'entra-ca-groups-unprotected', polarity: 'fail-means-met',
|
||||||
|
ruleText: "All security groups assigned to Conditional Access Policies should be protected by RMAU.", sources: [maesterTest('MT.1035')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1049', conditionId: 'entra-ca-policy-integrity-gaps', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Conditional Access policies for User Risk and Sign-in Risk should be configured separately.", sources: [maesterTest('MT.1049')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1061', conditionId: 'entra-ca-policy-integrity-gaps', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Device registration MFA control conflicts with Conditional Access policies.", sources: [maesterTest('MT.1061')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1067', conditionId: 'entra-ca-policy-integrity-gaps', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Authentication method policies should not reference non-existent groups.", sources: [maesterTest('MT.1067')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1072', conditionId: 'entra-ca-policy-integrity-gaps', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Conditional access policies should not use the deprecated Approved Client App grant.", sources: [maesterTest('MT.1072')] },
|
||||||
|
{ tool: 'maester', ruleId: 'CISA.MS.AAD.7.5', conditionId: 'entra-pim-controls-weak', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Provisioning users to highly privileged roles SHALL NOT occur outside of a PAM system.", sources: [SCUBA_AAD] },
|
||||||
|
{ tool: 'maester', ruleId: 'CISA.MS.AAD.7.6', conditionId: 'entra-pim-controls-weak', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Activation of the Global Administrator role SHALL require approval.", sources: [SCUBA_AAD] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1031', conditionId: 'entra-pim-controls-weak', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Privileged role on Control Plane are managed by PIM only.", sources: [maesterTest('MT.1031')] },
|
||||||
|
{ tool: 'maester', ruleId: 'CISA.MS.AAD.7.8', conditionId: 'entra-privileged-role-alerts-missing', polarity: 'fail-means-met',
|
||||||
|
ruleText: "User activation of the Global Administrator role SHALL trigger an alert.", sources: [SCUBA_AAD] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1026', conditionId: 'entra-admins-not-cloud-only', polarity: 'fail-means-met',
|
||||||
|
ruleText: "No hybrid user with permanent role assignment on Control Plane.", sources: [maesterTest('MT.1026')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1029', conditionId: 'entra-privileged-accounts-inactive', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Stale accounts are not assigned to privileged roles.", sources: [maesterTest('MT.1029')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1024.oneAdmin', conditionId: 'entra-global-admin-count-outside-range', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Designate more than one global admin.", sources: [maesterTest('MT.1024')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1024.roleOverlap', conditionId: 'entra-global-admin-overused', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Use least privileged administrative roles .", sources: [maesterTest('MT.1024')] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1024.servicePrincipalKeyExpiry', conditionId: 'entra-app-credentials-expiring', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Renew expiring service principal credentials.", sources: [maesterTest('MT.1024')] },
|
||||||
|
{ tool: 'maester', ruleId: 'CISA.MS.AAD.5.1', conditionId: 'entra-app-creation-unrestricted', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Only administrators SHALL be allowed to register applications.", sources: [SCUBA_AAD] },
|
||||||
|
{ tool: 'maester', ruleId: 'EIDSCA.AP10', conditionId: 'entra-app-creation-unrestricted', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Default Authorization Settings - Default User Role Permissions - Allowed to create Apps.", sources: [EIDSCA_TESTS] },
|
||||||
|
{ tool: 'maester', ruleId: 'MT.1002', conditionId: 'entra-app-creation-unrestricted', polarity: 'fail-means-met',
|
||||||
|
ruleText: "App management restrictions on applications and service principals is configured and enabled.", sources: [maesterTest('MT.1002')] },
|
||||||
|
{ tool: 'maester', ruleId: 'EIDSCA.AP08', conditionId: 'entra-users-can-consent-apps', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Default Authorization Settings - User consent policy assigned for applications.", sources: [EIDSCA_TESTS] },
|
||||||
|
{ tool: 'maester', ruleId: 'EIDSCA.AF01', conditionId: 'entra-mfa-not-phishing-resistant', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Authentication Method - FIDO2 security key - State.", sources: [EIDSCA_TESTS] },
|
||||||
|
{ tool: 'maester', ruleId: 'EIDSCA.AG03', conditionId: 'entra-mfa-fatigue-protection-weak', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Authentication Method - General Settings - Report suspicious activity - Included users/groups.", sources: [EIDSCA_TESTS] },
|
||||||
|
{ tool: 'maester', ruleId: 'EIDSCA.AM03', conditionId: 'entra-mfa-fatigue-protection-weak', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Authentication Method - Microsoft Authenticator - Require number matching for push notifications.", sources: [EIDSCA_TESTS] },
|
||||||
|
{ tool: 'maester', ruleId: 'EIDSCA.AM04', conditionId: 'entra-mfa-fatigue-protection-weak', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Authentication Method - Microsoft Authenticator - Included users/groups of number matching for push notifications.", sources: [EIDSCA_TESTS] },
|
||||||
|
{ tool: 'maester', ruleId: 'EIDSCA.AM06', conditionId: 'entra-mfa-fatigue-protection-weak', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Authentication Method - Microsoft Authenticator - Show application name in push and passwordless notifications.", sources: [EIDSCA_TESTS] },
|
||||||
|
{ tool: 'maester', ruleId: 'EIDSCA.AM07', conditionId: 'entra-mfa-fatigue-protection-weak', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Authentication Method - Microsoft Authenticator - Included users/groups to show application name in push and passwordless notifications.", sources: [EIDSCA_TESTS] },
|
||||||
|
{ tool: 'maester', ruleId: 'EIDSCA.AM09', conditionId: 'entra-mfa-fatigue-protection-weak', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Authentication Method - Microsoft Authenticator - Show geographic location in push and passwordless notifications.", sources: [EIDSCA_TESTS] },
|
||||||
|
{ tool: 'maester', ruleId: 'EIDSCA.AM10', conditionId: 'entra-mfa-fatigue-protection-weak', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Authentication Method - Microsoft Authenticator - Included users/groups to show geographic location in push and passwordless notifications.", sources: [EIDSCA_TESTS] },
|
||||||
|
{ tool: 'maester', ruleId: 'EIDSCA.AS04', conditionId: 'entra-weak-mfa-methods-enabled', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Authentication Method - SMS - Use for sign-in.", sources: [EIDSCA_TESTS] },
|
||||||
|
{ tool: 'maester', ruleId: 'EIDSCA.AV01', conditionId: 'entra-weak-mfa-methods-enabled', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Authentication Method - Voice call - State.", sources: [EIDSCA_TESTS] },
|
||||||
|
{ tool: 'maester', ruleId: 'EIDSCA.PR02', conditionId: 'entra-password-protection-not-enforced', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Default Settings - Password Rule Settings - Password Protection - Enable password protection on Windows Server Active Directory.", sources: [EIDSCA_PASSWORD_RULES] },
|
||||||
|
{ tool: 'cis-cat', ruleId: '19.7.5.1', conditionId: 'mark-of-the-web-suppressed', polarity: 'fail-means-met',
|
||||||
|
ruleText: "Ensure 'Do not preserve zone information in file attachments' is set to 'Disabled'",
|
||||||
|
requiredTitleFragment: "'Do not preserve zone information in file attachments'", sources: [CIS_WINDOWS_SERVER] },
|
||||||
{ tool: 'pingcastle', ruleId: 'A-NoServicePolicy', conditionId: 'ad-service-accounts-no-strong-policy', polarity: 'fail-means-met',
|
{ tool: 'pingcastle', ruleId: 'A-NoServicePolicy', conditionId: 'ad-service-accounts-no-strong-policy', polarity: 'fail-means-met',
|
||||||
ruleText: 'No password policy for service accounts found (MinimumPasswordLength>=20)', sources: [PINGCASTLE_RULES] },
|
ruleText: 'No password policy for service accounts found (MinimumPasswordLength>=20)', sources: [PINGCASTLE_RULES] },
|
||||||
{ tool: 'purple-knight', ruleId: 'SI000096', conditionId: 'ad-privileged-weak-password-policy', polarity: 'fail-means-met',
|
{ tool: 'purple-knight', ruleId: 'SI000096', conditionId: 'ad-privileged-weak-password-policy', polarity: 'fail-means-met',
|
||||||
|
|
@ -2507,7 +3052,19 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
{ tool: 'cis-cat', sectionTitle: "Windows Update", conditionId: 'cis-windows-update-unmet' },
|
{ tool: 'cis-cat', sectionTitle: "Windows Update", conditionId: 'cis-windows-update-unmet' },
|
||||||
{ tool: 'cis-cat', sectionTitle: "Group Policy", conditionId: 'cis-policy-processing-unmet' },
|
{ tool: 'cis-cat', sectionTitle: "Group Policy", conditionId: 'cis-policy-processing-unmet' },
|
||||||
{ tool: 'cis-cat', sectionTitle: "Windows Time Service", conditionId: 'cis-time-sync-unmet' },
|
{ tool: 'cis-cat', sectionTitle: "Windows Time Service", conditionId: 'cis-time-sync-unmet' },
|
||||||
{ tool: 'cis-cat', sectionTitle: "AutoPlay Policies", conditionId: 'autorun-enabled' }
|
{ tool: 'cis-cat', sectionTitle: "AutoPlay Policies", conditionId: 'autorun-enabled' },
|
||||||
|
{ tool: 'cis-cat', sectionTitle: "Password Policy", conditionId: 'cis-password-policy-unmet' },
|
||||||
|
{ tool: 'cis-cat', sectionTitle: "Account Lockout Policy", conditionId: 'cis-password-policy-unmet' },
|
||||||
|
{ tool: 'cis-cat', sectionTitle: "Domain member", conditionId: 'cis-authentication-hardening-unmet' },
|
||||||
|
{ tool: 'cis-cat', sectionTitle: "Microsoft network client", conditionId: 'cis-smb-hardening-unmet' },
|
||||||
|
{ tool: 'cis-cat', sectionTitle: "Microsoft network server", conditionId: 'cis-smb-hardening-unmet' },
|
||||||
|
{ tool: 'cis-cat', sectionTitle: "Network access", conditionId: 'cis-anonymous-access-unmet' },
|
||||||
|
{ tool: 'cis-cat', sectionTitle: "User Account Control", conditionId: 'cis-local-privilege-hardening-unmet' },
|
||||||
|
{ tool: 'cis-cat', sectionTitle: "System objects", conditionId: 'cis-local-privilege-hardening-unmet' },
|
||||||
|
{ tool: 'cis-cat', sectionTitle: "Shutdown", conditionId: 'cis-device-access-unmet' },
|
||||||
|
{ tool: 'cis-cat', sectionTitle: "Devices", conditionId: 'cis-print-hardening-unmet' },
|
||||||
|
{ tool: 'cis-cat', sectionTitle: "Attachment Manager", conditionId: 'cis-defender-unmet' },
|
||||||
|
{ tool: 'cis-cat', sectionTitle: "Playback", conditionId: 'cis-internet-communication-unmet' }
|
||||||
],
|
],
|
||||||
gpoVerdictRules: [
|
gpoVerdictRules: [
|
||||||
{ target: { mechanism: 'security-template', scope: 'computer', section: "Privilege Rights", key: "SeBackupPrivilege" }, conditionId: 'gpo-privileged-rights-deviate' },
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Privilege Rights", key: "SeBackupPrivilege" }, conditionId: 'gpo-privileged-rights-deviate' },
|
||||||
|
|
@ -2551,7 +3108,96 @@ export const RELEASED_ASSESSMENT_CONTEXT_CATALOG: AssessmentContextCatalog = {
|
||||||
{ target: { mechanism: 'security-template', scope: 'computer', section: "System Access", key: "PasswordHistorySize" }, conditionId: 'gpo-password-lifetime-deviates' },
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "System Access", key: "PasswordHistorySize" }, conditionId: 'gpo-password-lifetime-deviates' },
|
||||||
{ target: { mechanism: 'security-template', scope: 'computer', section: "System Access", key: "ClearTextPassword" }, conditionId: 'gpo-reversible-password-storage-deviates' },
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "System Access", key: "ClearTextPassword" }, conditionId: 'gpo-reversible-password-storage-deviates' },
|
||||||
{ target: { mechanism: 'security-template', scope: 'computer', section: "System Access", key: "ForceLogoffWhenHourExpire" }, conditionId: 'gpo-logon-hours-enforcement-deviates' },
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "System Access", key: "ForceLogoffWhenHourExpire" }, conditionId: 'gpo-logon-hours-enforcement-deviates' },
|
||||||
{ target: { mechanism: 'registry-key', scope: 'user', key: "Software\\Policies\\Microsoft\\Office\\16.0\\Common", valueName: "fbaenabledhosts" }, conditionId: 'gpo-office-forms-auth-hosts-deviate' }
|
{ target: { mechanism: 'registry-key', scope: 'user', key: "Software\\Policies\\Microsoft\\Office\\16.0\\Common", valueName: "fbaenabledhosts" }, conditionId: 'gpo-office-forms-auth-hosts-deviate' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows\\System", valueName: "AllowDomainPINLogon" }, conditionId: 'gpo-convenience-pin-sign-in-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Privilege Rights", key: "SeTrustedCredManAccessPrivilege" }, conditionId: 'gpo-privileged-rights-deviate' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Privilege Rights", key: "SeCreateSymbolicLinkPrivilege" }, conditionId: 'gpo-privileged-rights-deviate' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Privilege Rights", key: "SeCreateGlobalPrivilege" }, conditionId: 'gpo-privileged-rights-deviate' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Privilege Rights", key: "SeCreatePermanentPrivilege" }, conditionId: 'gpo-privileged-rights-deviate' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Privilege Rights", key: "SeLockMemoryPrivilege" }, conditionId: 'gpo-privileged-rights-deviate' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Privilege Rights", key: "SeCreatePagefilePrivilege" }, conditionId: 'gpo-privileged-rights-deviate' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Privilege Rights", key: "SeProfileSingleProcessPrivilege" }, conditionId: 'gpo-privileged-rights-deviate' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Privilege Rights", key: "SeSystemProfilePrivilege" }, conditionId: 'gpo-privileged-rights-deviate' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Privilege Rights", key: "SeSystemtimePrivilege" }, conditionId: 'gpo-privileged-rights-deviate' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Privilege Rights", key: "SeIncreaseQuotaPrivilege" }, conditionId: 'gpo-privileged-rights-deviate' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Privilege Rights", key: "SeMachineAccountPrivilege" }, conditionId: 'gpo-privileged-rights-deviate' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Privilege Rights", key: "SeDenyBatchLogonRight" }, conditionId: 'gpo-privileged-rights-deviate' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Privilege Rights", key: "SeDenyServiceLogonRight" }, conditionId: 'gpo-privileged-rights-deviate' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows\\PowerShell" }, conditionId: 'gpo-powershell-logging-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows\\EventLog" }, conditionId: 'gpo-event-logging-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\Audit" }, conditionId: 'gpo-event-logging-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\SCENoApplyLegacyAuditPolicy" }, conditionId: 'gpo-audit-policy-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows NT\\Terminal Services" }, conditionId: 'gpo-remote-desktop-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows\\WinRM" }, conditionId: 'gpo-remote-management-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Services\\LanManServer\\Parameters\\EnableSecuritySignature" }, conditionId: 'gpo-smb-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Services\\LanmanWorkstation\\Parameters\\EnableSecuritySignature" }, conditionId: 'gpo-smb-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Services\\LanmanWorkstation\\Parameters\\EnablePlainTextPassword" }, conditionId: 'gpo-smb-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows\\LanmanWorkstation" }, conditionId: 'gpo-smb-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "System\\CurrentControlSet\\Services\\LanmanServer\\Parameters", valueName: "SMB1" }, conditionId: 'gpo-smb-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "System\\CurrentControlSet\\Services\\MrxSmb10" }, conditionId: 'gpo-smb-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Services\\Netlogon\\Parameters\\RequireSignOrSeal" }, conditionId: 'gpo-secure-channel-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Services\\Netlogon\\Parameters\\SealSecureChannel" }, conditionId: 'gpo-secure-channel-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Services\\Netlogon\\Parameters\\SignSecureChannel" }, conditionId: 'gpo-secure-channel-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Services\\Netlogon\\Parameters\\RequireStrongKey" }, conditionId: 'gpo-secure-channel-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Services\\Netlogon\\Parameters\\DisablePasswordChange" }, conditionId: 'gpo-secure-channel-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Services\\Netlogon\\Parameters\\MaximumPasswordAge" }, conditionId: 'gpo-secure-channel-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\MSV1_0\\NTLMMinClientSec" }, conditionId: 'gpo-ntlm-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\MSV1_0\\NTLMMinServerSec" }, conditionId: 'gpo-ntlm-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\MSV1_0\\AllowNullSessionFallback" }, conditionId: 'gpo-ntlm-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\pku2u\\AllowOnlineID" }, conditionId: 'gpo-ntlm-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\UseMachineId" }, conditionId: 'gpo-ntlm-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "System Access", key: "LSAAnonymousNameLookup" }, conditionId: 'gpo-anonymous-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "System Access", key: "EnableGuestAccount" }, conditionId: 'gpo-anonymous-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\RestrictAnonymous" }, conditionId: 'gpo-anonymous-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\RestrictAnonymousSAM" }, conditionId: 'gpo-anonymous-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\EveryoneIncludesAnonymous" }, conditionId: 'gpo-anonymous-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\ForceGuest" }, conditionId: 'gpo-anonymous-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Services\\LanManServer\\Parameters\\RestrictNullSessAccess" }, conditionId: 'gpo-anonymous-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Services\\LanManServer\\Parameters\\NullSessionPipes" }, conditionId: 'gpo-anonymous-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Services\\LanManServer\\Parameters\\NullSessionShares" }, conditionId: 'gpo-anonymous-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\SecurePipeServers\\Winreg\\AllowedExactPaths\\Machine" }, conditionId: 'gpo-anonymous-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\SecurePipeServers\\Winreg\\AllowedPaths\\Machine" }, conditionId: 'gpo-anonymous-access-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows NT\\Rpc" }, conditionId: 'gpo-anonymous-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\NoLMHash" }, conditionId: 'gpo-lm-hash-storage-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\Kerberos\\Parameters\\SupportedEncryptionTypes" }, conditionId: 'gpo-kerberos-encryption-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\EnableLUA" }, conditionId: 'gpo-local-privilege-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\ConsentPromptBehaviorAdmin" }, conditionId: 'gpo-local-privilege-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\ConsentPromptBehaviorUser" }, conditionId: 'gpo-local-privilege-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\EnableInstallerDetection" }, conditionId: 'gpo-local-privilege-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\EnableSecureUIAPaths" }, conditionId: 'gpo-local-privilege-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\PromptOnSecureDesktop" }, conditionId: 'gpo-local-privilege-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\EnableVirtualization" }, conditionId: 'gpo-local-privilege-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\FilterAdministratorToken" }, conditionId: 'gpo-local-privilege-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\EnableUIADesktopToggle" }, conditionId: 'gpo-local-privilege-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\Session Manager\\ProtectionMode" }, conditionId: 'gpo-local-privilege-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\Session Manager\\Kernel\\ObCaseInsensitive" }, conditionId: 'gpo-local-privilege-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", valueName: "LocalAccountTokenFilterPolicy" }, conditionId: 'gpo-local-privilege-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows\\Installer" }, conditionId: 'gpo-local-privilege-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'user', key: "Software\\Policies\\Microsoft\\Windows\\Installer" }, conditionId: 'gpo-local-privilege-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "System\\CurrentControlSet\\Control\\Lsa", valueName: "RunAsPPL" }, conditionId: 'gpo-credential-protection-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows\\DeviceGuard" }, conditionId: 'gpo-credential-protection-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows\\CredentialsDelegation" }, conditionId: 'gpo-credential-protection-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "System\\CurrentControlSet\\Control\\SecurityProviders\\WDigest" }, conditionId: 'gpo-credential-protection-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\DisableDomainCreds" }, conditionId: 'gpo-credential-protection-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\CachedLogonsCount" }, conditionId: 'gpo-credential-protection-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows NT\\Printers" }, conditionId: 'gpo-print-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\System\\CurrentControlSet\\Control\\Print\\Providers\\LanMan Print Services\\Servers\\AddPrinterDrivers" }, conditionId: 'gpo-print-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows Defender" }, conditionId: 'gpo-malware-protection-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Edge" }, conditionId: 'gpo-malware-protection-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows\\Explorer" }, conditionId: 'gpo-malware-protection-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer" }, conditionId: 'gpo-malware-protection-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows\\System", valueName: "EnableSmartScreen" }, conditionId: 'gpo-malware-protection-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows\\System", valueName: "ShellSmartScreenLevel" }, conditionId: 'gpo-malware-protection-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'user', key: "Software\\Policies\\Microsoft\\Office\\16.0" }, conditionId: 'gpo-office-hardening-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows\\DeviceInstall" }, conditionId: 'gpo-local-device-access-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\FVE" }, conditionId: 'gpo-local-device-access-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows\\Kernel DMA Protection" }, conditionId: 'gpo-local-device-access-deviates' },
|
||||||
|
{ target: { mechanism: 'registry-key', scope: 'computer', key: "Software\\Policies\\Microsoft\\Windows\\Personalization" }, conditionId: 'gpo-local-device-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\ShutdownWithoutLogon" }, conditionId: 'gpo-local-device-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\InactivityTimeoutSecs" }, conditionId: 'gpo-local-device-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\DisableCAD" }, conditionId: 'gpo-local-device-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\DontDisplayLastUserName" }, conditionId: 'gpo-local-device-access-deviates' },
|
||||||
|
{ target: { mechanism: 'security-template', scope: 'computer', section: "Registry Values", key: "MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\ScRemoveOption" }, conditionId: 'gpo-local-device-access-deviates' }
|
||||||
],
|
],
|
||||||
primaryClusters: {
|
primaryClusters: {
|
||||||
'adcs-web-enrollment-esc8': 'adcs-escalation',
|
'adcs-web-enrollment-esc8': 'adcs-escalation',
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,9 @@ import { contextClusterSeverities } from './context-cluster-severity';
|
||||||
* The version changes with every recognition vocabulary release, because consumers (for example stored work-package
|
* The version changes with every recognition vocabulary release, because consumers (for example stored work-package
|
||||||
* snapshots) key their results by this version and the catalogue version, not by the vocabulary.
|
* snapshots) key their results by this version and the catalogue version, not by the vocabulary.
|
||||||
*/
|
*/
|
||||||
export const ASSESSMENT_CONTEXT_ENGINE = { id: 'assessment-context-engine', version: '1.12.0' } as const;
|
export const ASSESSMENT_CONTEXT_ENGINE = { id: 'assessment-context-engine', version: '1.13.0' } as const;
|
||||||
/** Vocabulary release the engine version above was published with; a vocabulary change needs a new engine version. */
|
/** Vocabulary release the engine version above was published with; a vocabulary change needs a new engine version. */
|
||||||
export const ASSESSMENT_CONTEXT_ENGINE_VOCABULARY = '1.2.0';
|
export const ASSESSMENT_CONTEXT_ENGINE_VOCABULARY = '1.3.0';
|
||||||
|
|
||||||
export type DeclaredValue =
|
export type DeclaredValue =
|
||||||
| { state: 'configured'; value: number }
|
| { state: 'configured'; value: number }
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
* catalogued rule text and report section title (tests/assessment-context/context-recognition.test.ts); a term
|
* catalogued rule text and report section title (tests/assessment-context/context-recognition.test.ts); a term
|
||||||
* whose matches contradict the catalogue is narrowed or removed, never kept for recall.
|
* whose matches contradict the catalogue is narrowed or removed, never kept for recall.
|
||||||
*/
|
*/
|
||||||
export const CONTEXT_RECOGNITION_VOCABULARY = { id: 'assessment-context-recognition', version: '1.2.0' } as const;
|
export const CONTEXT_RECOGNITION_VOCABULARY = { id: 'assessment-context-recognition', version: '1.3.0' } as const;
|
||||||
|
|
||||||
export interface RecognitionTerm {
|
export interface RecognitionTerm {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -30,7 +30,8 @@ export const CONTEXT_RECOGNITION_TERMS: RecognitionTerm[] = [
|
||||||
term('kerberoasting-exposure', 'kerberoast', /\bkerberoast/i),
|
term('kerberoasting-exposure', 'kerberoast', /\bkerberoast/i),
|
||||||
term('kerberoasting-exposure', 'spn', /\bSPNs?\b/, /delegation/i),
|
term('kerberoasting-exposure', 'spn', /\bSPNs?\b/, /delegation/i),
|
||||||
term('kerberoasting-exposure', 'as-rep', /\bAS-?REP\b|\bpre-?authentication\b/i),
|
term('kerberoasting-exposure', 'as-rep', /\bAS-?REP\b|\bpre-?authentication\b/i),
|
||||||
term('kerberoasting-exposure', 'gmsa', /\bg?MSAs?\b|\bmanaged service accounts?\b/i),
|
// Vocabulary 1.3.0: readable gMSA passwords are a permission finding; old gMSA passwords are an age finding.
|
||||||
|
term('kerberoasting-exposure', 'gmsa', /\bg?MSAs?\b|\bmanaged service accounts?\b/i, /\baccess to gMSA passwords\b|\bgMSA objects with old passwords\b/i),
|
||||||
term('kerberoasting-exposure', 'rc4', /\bRC4\b/i),
|
term('kerberoasting-exposure', 'rc4', /\bRC4\b/i),
|
||||||
term('kerberoasting-exposure', 'des-encryption', /\bDES\b(?! ?\()/),
|
term('kerberoasting-exposure', 'des-encryption', /\bDES\b(?! ?\()/),
|
||||||
|
|
||||||
|
|
@ -64,18 +65,18 @@ export const CONTEXT_RECOGNITION_TERMS: RecognitionTerm[] = [
|
||||||
term('ad-privileged-account-exposure', 'dns-admins', /\bDNS ?Admins\b/i),
|
term('ad-privileged-account-exposure', 'dns-admins', /\bDNS ?Admins\b/i),
|
||||||
term('ad-privileged-account-exposure', 'shadow-credentials', /\bshadow credentials\b/i),
|
term('ad-privileged-account-exposure', 'shadow-credentials', /\bshadow credentials\b/i),
|
||||||
term('ad-privileged-account-exposure', 'builtin-admin', /\b(?:built-in|native) (?:domain )?administrator(?: account)?\b/i,
|
term('ad-privileged-account-exposure', 'builtin-admin', /\b(?:built-in|native) (?:domain )?administrator(?: account)?\b/i,
|
||||||
/\buser account control\b|\badmin approval mode\b/i),
|
/\buser account control\b|\badmin approval mode\b|\bold password/i),
|
||||||
term('ad-privileged-account-exposure', 'dcsync', /\bDC ?Sync\b|\breplication rights\b/i),
|
term('ad-privileged-account-exposure', 'dcsync', /\bDC ?Sync\b|\breplication rights\b/i),
|
||||||
term('ad-privileged-account-exposure', 'privileged-objects', /\bprivileged (?:objects?|groups?)\b|\boperators groups?\b|\bdomain admins?\b|\benterprise admins\b/i,
|
term('ad-privileged-account-exposure', 'privileged-objects', /\bprivileged (?:objects?|groups?)\b|\boperators groups?\b|\bdomain admins?\b|\benterprise admins\b/i,
|
||||||
/\bcloud\b|\bEntra\b|\brole\b|\bmembership\b|\bchanges?\b|\bnon-expiring\b|\bnever[- ]expir/i),
|
/\bcloud\b|\bEntra\b|\brole\b|\bmembership\b|\bchanges?\b|\bnon-expiring\b|\bnever[- ]expir|\badminCount\b/i),
|
||||||
term('ad-privileged-account-exposure', 'admin-accounts', /\badmin accounts?\b|\badmins with\b/i,
|
term('ad-privileged-account-exposure', 'admin-accounts', /\badmin accounts?\b|\badmins with\b/i,
|
||||||
/\bcloud\b|\bEntra\b|\bglobal admin|\bsensitive and cannot\b|\bAutoAdminLogon\b|\bbuilt-in\b|\bnative\b/i),
|
/\bcloud\b|\bEntra\b|\bglobal admin|\bsensitive and cannot\b|\bAutoAdminLogon\b|\bbuilt-in\b|\bnative\b|\bold passwords?\b/i),
|
||||||
|
|
||||||
// AD persistence indicators.
|
// AD persistence indicators.
|
||||||
term('ad-persistence-indicators', 'adminsdholder', /\bAdminSDHolder\b|\badminCount\b/i),
|
term('ad-persistence-indicators', 'adminsdholder', /\bAdminSDHolder\b|\badminCount\b/i),
|
||||||
term('ad-persistence-indicators', 'krbtgt', /\bkrbtgt\b|\bKRBTGT\b/, /\bdelegation\b|\bRBCD\b/i),
|
term('ad-persistence-indicators', 'krbtgt', /\bkrbtgt\b|\bKRBTGT\b/, /\bdelegation\b|\bRBCD\b/i),
|
||||||
term('ad-persistence-indicators', 'suspicious', /\bsuspicious\b|\babnormal\b|\bephemeral\b|\breanimate\b|\bSID ?History\b|\bprimary group IDs?\b/i,
|
term('ad-persistence-indicators', 'suspicious', /\bsuspicious\b|\babnormal\b|\bephemeral\b|\breanimate\b|\bSID ?History\b|\bprimary group IDs?\b/i,
|
||||||
/\breport suspicious activity\b/i),
|
/\breport suspicious activity\b|\btrust with SID History\b/i),
|
||||||
|
|
||||||
// AD reconnaissance.
|
// AD reconnaissance.
|
||||||
term('ad-reconnaissance-exposure', 'anonymous', /\banonym\w*/i, /\bTeams\b|\bmeetings?\b|\bsharing links?\b|\bSharePoint\b|\bOneDrive\b/i),
|
term('ad-reconnaissance-exposure', 'anonymous', /\banonym\w*/i, /\bTeams\b|\bmeetings?\b|\bsharing links?\b|\bSharePoint\b|\bOneDrive\b/i),
|
||||||
|
|
@ -96,7 +97,7 @@ export const CONTEXT_RECOGNITION_TERMS: RecognitionTerm[] = [
|
||||||
term('password-guessing-exposure', 'lockout', /\blockout\b/i, /\bsmart lockout\b|\baudit\b/i),
|
term('password-guessing-exposure', 'lockout', /\blockout\b/i, /\bsmart lockout\b|\baudit\b/i),
|
||||||
term('password-guessing-exposure', 'never-expires', /\bpassword never expires\b|\bnever[- ]expiring passwords?\b|\bnon-expiring passwords?\b|\bpassword that never expires\b/i),
|
term('password-guessing-exposure', 'never-expires', /\bpassword never expires\b|\bnever[- ]expiring passwords?\b|\bnon-expiring passwords?\b|\bpassword that never expires\b/i),
|
||||||
term('password-guessing-exposure', 'password-not-required', /\bpassword not required\b|\bPASSWD_NOTREQD\b/i),
|
term('password-guessing-exposure', 'password-not-required', /\bpassword not required\b|\bPASSWD_NOTREQD\b/i),
|
||||||
term('password-guessing-exposure', 'old-passwords', /\bold passwords?\b|\bpassword last set\b|\bpasswords? older than\b/i, /\bcomputers?\b|\bSSO\b|\bkrbtgt\b/i),
|
term('password-guessing-exposure', 'old-passwords', /\bold passwords?\b|\bpassword last set\b|\bpasswords? older than\b/i, /\bcomputers?\b|\bSSO\b|\bkrbtgt\b|\btrust\b/i),
|
||||||
term('password-guessing-exposure', 'weak-password', /\bweak password\b/i),
|
term('password-guessing-exposure', 'weak-password', /\bweak password\b/i),
|
||||||
term('password-guessing-exposure', 'armoring', /\bKerberos armoring\b|\bFAST\b/),
|
term('password-guessing-exposure', 'armoring', /\bKerberos armoring\b|\bFAST\b/),
|
||||||
|
|
||||||
|
|
@ -137,7 +138,7 @@ export const CONTEXT_RECOGNITION_TERMS: RecognitionTerm[] = [
|
||||||
term('patch-configuration-drift', 'functional-level', /\bfunctional levels?\b/i, /\barmoring\b/i),
|
term('patch-configuration-drift', 'functional-level', /\bfunctional levels?\b/i, /\barmoring\b/i),
|
||||||
|
|
||||||
// Cloud privileged access.
|
// Cloud privileged access.
|
||||||
term('cloud-privileged-access-exposure', 'global-admin', /\bglobal admin(?:istrator)?s?\b/i),
|
term('cloud-privileged-access-exposure', 'global-admin', /\bglobal admin(?:istrator)?s?\b/i, /\btrigger an alert\b/i),
|
||||||
term('cloud-privileged-access-exposure', 'control-plane', /\bcontrol plane\b/i),
|
term('cloud-privileged-access-exposure', 'control-plane', /\bcontrol plane\b/i),
|
||||||
term('cloud-privileged-access-exposure', 'role-assignment', /\brole assignments?\b|\bhighly privileged roles?\b/i, /\btrigger an alert\b|\bphishing-resistant\b/i),
|
term('cloud-privileged-access-exposure', 'role-assignment', /\brole assignments?\b|\bhighly privileged roles?\b/i, /\btrigger an alert\b|\bphishing-resistant\b/i),
|
||||||
term('cloud-privileged-access-exposure', 'cloud-only', /\bcloud-only\b/i),
|
term('cloud-privileged-access-exposure', 'cloud-only', /\bcloud-only\b/i),
|
||||||
|
|
@ -146,7 +147,7 @@ export const CONTEXT_RECOGNITION_TERMS: RecognitionTerm[] = [
|
||||||
|
|
||||||
// Cloud sign-in protection.
|
// Cloud sign-in protection.
|
||||||
term('cloud-sign-in-protection-gaps', 'legacy-auth', /\blegacy authentication\b/i),
|
term('cloud-sign-in-protection-gaps', 'legacy-auth', /\blegacy authentication\b/i),
|
||||||
term('cloud-sign-in-protection-gaps', 'mfa', /\bMFA\b|\bmulti-?factor\b/i, /\badministrat\w*|\bhighly privileged\b/i),
|
term('cloud-sign-in-protection-gaps', 'mfa', /\bMFA\b|\bmulti-?factor\b/i, /\badministrat\w*|\badmins\b|\bhighly privileged\b/i),
|
||||||
term('cloud-sign-in-protection-gaps', 'risk-policy', /\bsign-in risk\b|\buser risk\b|\brisky sign-ins?\b|\bhigh risk\b|\binsider risk\b/i),
|
term('cloud-sign-in-protection-gaps', 'risk-policy', /\bsign-in risk\b|\buser risk\b|\brisky sign-ins?\b|\bhigh risk\b|\binsider risk\b/i),
|
||||||
term('cloud-sign-in-protection-gaps', 'auth-methods', /\bauthentication methods?\b|\bFIDO2\b|\bMicrosoft Authenticator\b|\bone-time passcode\b/i),
|
term('cloud-sign-in-protection-gaps', 'auth-methods', /\bauthentication methods?\b|\bFIDO2\b|\bMicrosoft Authenticator\b|\bone-time passcode\b/i),
|
||||||
term('cloud-sign-in-protection-gaps', 'cloud-password', /\bsmart lockout\b|\bpassword protection\b/i),
|
term('cloud-sign-in-protection-gaps', 'cloud-password', /\bsmart lockout\b|\bpassword protection\b/i),
|
||||||
|
|
@ -158,7 +159,7 @@ export const CONTEXT_RECOGNITION_TERMS: RecognitionTerm[] = [
|
||||||
term('cloud-app-consent-exposure', 'app-credentials', /\bapplication credentials\b|\bunused applications\b|\bservice principal credentials\b/i),
|
term('cloud-app-consent-exposure', 'app-credentials', /\bapplication credentials\b|\bunused applications\b|\bservice principal credentials\b/i),
|
||||||
|
|
||||||
// Cloud external access.
|
// Cloud external access.
|
||||||
term('cloud-external-access-exposure', 'guests', /\bguest (?:users?|invites?|inviter|access)\b|\binvite guests?\b/i),
|
term('cloud-external-access-exposure', 'guests', /\bguest (?:users?|invites?|inviter|access)\b|\binvite guests?\b/i, /\brequire MFA\b/i),
|
||||||
term('cloud-external-access-exposure', 'external', /\bexternal (?:users?|domains?|sharing)\b/i, /\brole assignments?\b|\bcontrol plane\b|\bforward\w*/i),
|
term('cloud-external-access-exposure', 'external', /\bexternal (?:users?|domains?|sharing)\b/i, /\brole assignments?\b|\bcontrol plane\b|\bforward\w*/i),
|
||||||
term('cloud-external-access-exposure', 'teams-meetings', /\bTeams meetings?\b/i),
|
term('cloud-external-access-exposure', 'teams-meetings', /\bTeams meetings?\b/i),
|
||||||
term('cloud-external-access-exposure', 'self-signup', /\bemail validation\b|\bemail based subscription\b|\bpublic groups\b/i),
|
term('cloud-external-access-exposure', 'self-signup', /\bemail validation\b|\bemail based subscription\b|\bpublic groups\b/i),
|
||||||
|
|
@ -220,10 +221,19 @@ export const CONTEXT_RECOGNITION_TERMS: RecognitionTerm[] = [
|
||||||
term('malicious-code-execution-exposure', 'defender-scan', /\b(?:full|quick) scan\b|\bmalware\b|\bscan (?:removable|mapped) (?:network )?drives?\b/i,
|
term('malicious-code-execution-exposure', 'defender-scan', /\b(?:full|quick) scan\b|\bmalware\b|\bscan (?:removable|mapped) (?:network )?drives?\b/i,
|
||||||
/\bmalware filter\w*|\banti-?malware polic\w*|\bcommon attachment\w*/i),
|
/\bmalware filter\w*|\banti-?malware polic\w*|\bcommon attachment\w*/i),
|
||||||
term('ad-recovery-gaps', 'inactive-dc', /\bDCs? inactive\b|\binactive (?:DCs?|domain controllers?)\b/i),
|
term('ad-recovery-gaps', 'inactive-dc', /\bDCs? inactive\b|\binactive (?:DCs?|domain controllers?)\b/i),
|
||||||
term('ad-recovery-gaps', 'trust', /\btrusts?\b/i, /\bfor delegation\b|\bTrust Center\b|\bzero trust\b/i),
|
term('ad-recovery-gaps', 'trust', /\btrusts?\b/i, /\bfor delegation\b|\bTrust Center\b|\bzero trust\b|\bServer Trust Account\b/i),
|
||||||
term('ad-privileged-account-exposure', 'ou-delegated-permission', /\bdelegated permissions? on (?:AD )?(?:OUs?|organizational units?)\b|\bpermissions? (?:on|over) (?:AD )?(?:OUs?|organizational units?)\b|\bOU delegation\b/i,
|
term('ad-privileged-account-exposure', 'ou-delegated-permission', /\bdelegated permissions? on (?:AD )?(?:OUs?|organizational units?)\b|\bpermissions? (?:on|over) (?:AD )?(?:OUs?|organizational units?)\b|\bOU delegation\b/i,
|
||||||
/\bGraph\b|\bapplications?\b|\bapps?\b|\bconsent\b|\bEntra\b|\bAPI\b/i),
|
/\bGraph\b|\bapplications?\b|\bapps?\b|\bconsent\b|\bEntra\b|\bAPI\b/i),
|
||||||
term('detection-blind-spots', 'mdi-deception', /\bhoneytokens?\b|\bhoney ?pots?\b|\bVPN integration\b/i),
|
term('detection-blind-spots', 'mdi-deception', /\bhoneytokens?\b|\bhoney ?pots?\b|\bVPN integration\b/i),
|
||||||
|
// Catalogue 1.12.0 (vocabulary 1.3.0): distinctive words of the newly catalogued Purple Knight and Maester rules.
|
||||||
|
term('password-guessing-exposure', 'recoverable-passwords', /\breversible (?:encryption|passwords?)\b|\buserPassword\b|\bLM Hash\b/i),
|
||||||
|
term('ad-persistence-indicators', 'directory-tampering', /\bDCShadow\b|\bdisplay specifiers\b|\bdefault security descriptor\b|\bprivileged account creation\b|\breadable PGID\b/i),
|
||||||
|
term('ad-privileged-account-exposure', 'sensitive-object-rights', /\bRODC\b|\bDPAPI\b|\bschema permissions\b|\bServer Trust Account\b|\bgMSA passwords\b|\bowner is not an administrator\b|\bmore than \d+ privileged accounts\b|\bprivileged users that are disabled\b/i),
|
||||||
|
term('adcs-escalation', 'certificate-mapping', /\baltSecurityIdentities\b/i),
|
||||||
|
term('ad-recovery-gaps', 'dc-state', /\bDomain Controllers? in inconsistent state\b|\bnot authenticated to the domain\b/i),
|
||||||
|
term('malicious-code-execution-exposure', 'gpo-execution', /\blogon script path\b|\bwritable shortcuts?\b/i),
|
||||||
|
term('cloud-sign-in-protection-gaps', 'password-expiry', /\bexpire passwords\b|\bpasswords SHALL NOT expire\b|\bpassword expiration policy\b/i),
|
||||||
|
term('cloud-app-consent-exposure', 'app-management', /\bapp management restrictions\b/i),
|
||||||
// Entra identity governance and workload identities map to the existing cloud clusters (user decision 2026-10-12):
|
// Entra identity governance and workload identities map to the existing cloud clusters (user decision 2026-10-12):
|
||||||
// access packages, entitlement catalogs, pending access approvals and dynamic group rule attributes grant access;
|
// access packages, entitlement catalogs, pending access approvals and dynamic group rule attributes grant access;
|
||||||
// managed identities replace stored application secrets.
|
// managed identities replace stored application secrets.
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@ import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
// One activation order for floating data windows and Results; Filters has its own uppermost layer.
|
// One activation order for floating data windows and Results; Filters has its own uppermost layer.
|
||||||
let nextLayer = 3000;
|
let nextLayer = 3000;
|
||||||
|
// Reserve the next integer for the active Result inside a shared cluster.
|
||||||
|
const allocateLayer = () => (nextLayer += 2);
|
||||||
export function useFloatingWindowLayer(activation?: number) {
|
export function useFloatingWindowLayer(activation?: number) {
|
||||||
const [layer, setLayer] = useState(() => ++nextLayer);
|
const [layer, setLayer] = useState(allocateLayer);
|
||||||
const previous = useRef(activation);
|
const previous = useRef(activation);
|
||||||
const activate = () => setLayer(++nextLayer);
|
const activate = () => setLayer(allocateLayer());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (previous.current === activation) return;
|
if (previous.current === activation) return;
|
||||||
previous.current = activation;
|
previous.current = activation;
|
||||||
|
|
|
||||||
|
|
@ -1397,12 +1397,21 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
|
||||||
const context = impactContext.status === 'ready' ? publishContextReport(impactContext.context) : undefined;
|
const context = impactContext.status === 'ready' ? publishContextReport(impactContext.context) : undefined;
|
||||||
if (context && context.repositoryRevision !== health.repositoryRevision) throw new Error('The assessment data changed. Reload the client before exporting Security.');
|
if (context && context.repositoryRevision !== health.repositoryRevision) throw new Error('The assessment data changed. Reload the client before exporting Security.');
|
||||||
const generatedAt = new Date().toLocaleString('de-DE', { timeZone: 'Europe/Berlin' });
|
const generatedAt = new Date().toLocaleString('de-DE', { timeZone: 'Europe/Berlin' });
|
||||||
|
const selectedAssessmentIds = new Set([
|
||||||
|
...(app.activeAssessmentIds ?? []),
|
||||||
|
...history.scanRuns.filter(run => app.activeScanRunIds?.includes(run.id)).map(run => run.assessmentId)
|
||||||
|
]);
|
||||||
|
const selectedAssessments = history.assessments.filter(assessment => selectedAssessmentIds.has(assessment.id));
|
||||||
const snapshot = Object.freeze({ ...captured, width,
|
const snapshot = Object.freeze({ ...captured, width,
|
||||||
theme: document.documentElement.dataset.theme === 'dark' ? 'dark' as const : 'light' as const,
|
theme: document.documentElement.dataset.theme === 'dark' ? 'dark' as const : 'light' as const,
|
||||||
density: document.documentElement.dataset.density,
|
density: document.documentElement.dataset.density,
|
||||||
client: project.name,
|
client: project.name,
|
||||||
assessments: history.assessments.filter(assessment => app.activeAssessmentIds?.includes(assessment.id))
|
assessments: selectedAssessments
|
||||||
.map(assessment => assessment.name + (assessment.effectiveAt ? ' · ' + assessment.effectiveAt : '')).join(' · ') || app.activeAssessmentIds?.join(' · ') || '—',
|
.map(assessment => assessment.name + (assessment.effectiveAt ? ' · ' + assessment.effectiveAt : '')).join(' · ') || app.activeAssessmentIds?.join(' · ') || '—',
|
||||||
|
assessmentDetails: selectedAssessments
|
||||||
|
.map(assessment => ({ name: assessment.name, effectiveAt: assessment.effectiveAt })),
|
||||||
|
distribution: { total: securityView.total, domains: securityView.domains.map(({ label, count }) => ({ label, count })),
|
||||||
|
categories: securityView.groups.map(group => ({ label: group.label, count: group.records.length })) },
|
||||||
generatedAt, versions: [
|
generatedAt, versions: [
|
||||||
['Erstellt', generatedAt], ['Datenstand', 'Revision ' + health.repositoryRevision],
|
['Erstellt', generatedAt], ['Datenstand', 'Revision ' + health.repositoryRevision],
|
||||||
['Report Engine', REPORT_ENGINE_VERSION], ['Context Engine', context?.engineVersion ?? '—'],
|
['Report Engine', REPORT_ENGINE_VERSION], ['Context Engine', context?.engineVersion ?? '—'],
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,10 @@ export function captureSecurityView(view: ReactNode, width: number, appendix: Re
|
||||||
const style = document.createElement('style'); style.textContent = focusRules.join('\n'); clone.append(style);
|
const style = document.createElement('style'); style.textContent = focusRules.join('\n'); clone.append(style);
|
||||||
const popups = clone.querySelector('[data-security-export-popups]')!;
|
const popups = clone.querySelector('[data-security-export-popups]')!;
|
||||||
const popupMarkup = popups.innerHTML; popups.remove();
|
const popupMarkup = popups.innerHTML; popups.remove();
|
||||||
return { markup: clone.innerHTML, popups: popupMarkup };
|
// Publish the same complete facts separately so the report can place them before Findings.
|
||||||
|
clonedFacts?.style.removeProperty('padding-inline-start');
|
||||||
|
const environmentMarkup = clonedFacts?.outerHTML ?? '';
|
||||||
|
clonedFacts?.remove();
|
||||||
|
return { markup: clone.innerHTML, popups: popupMarkup, environmentMarkup };
|
||||||
} finally { flushSync(() => root.unmount()); host.remove(); }
|
} finally { flushSync(() => root.unmount()); host.remove(); }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import {
|
||||||
} from '../linked-results/result-window-layout';
|
} from '../linked-results/result-window-layout';
|
||||||
import { useFilteredRecords } from '../search-filter/use-filtered-records';
|
import { useFilteredRecords } from '../search-filter/use-filtered-records';
|
||||||
import { useVirtualRows } from '../../hooks/use-virtual-rows';
|
import { useVirtualRows } from '../../hooks/use-virtual-rows';
|
||||||
|
import { useFloatingWindowLayer } from '../../components/use-floating-window-layer';
|
||||||
import { toolIdForRecord } from '../../importers/status-normalization';
|
import { toolIdForRecord } from '../../importers/status-normalization';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -105,6 +106,8 @@ export function DatasetViewer() {
|
||||||
const records = useFilteredRecords();
|
const records = useFilteredRecords();
|
||||||
const minimizedWindows = useMemo(() => resultWindows.filter(w => w.minimized && !w.detached), [resultWindows]);
|
const minimizedWindows = useMemo(() => resultWindows.filter(w => w.minimized && !w.detached), [resultWindows]);
|
||||||
const clusteredWindows = useMemo(() => resultWindows.filter(w => !w.detached), [resultWindows]);
|
const clusteredWindows = useMemo(() => resultWindows.filter(w => !w.detached), [resultWindows]);
|
||||||
|
const clusterActivation = Math.max(0, ...clusteredWindows.map(item => item.zIndex));
|
||||||
|
const { layer: clusterLayer, activate: activateCluster } = useFloatingWindowLayer(clusterActivation);
|
||||||
const effectiveClusterWidth = Math.min(clusterWidth, Math.max(0, viewportSize.width - 12));
|
const effectiveClusterWidth = Math.min(clusterWidth, Math.max(0, viewportSize.width - 12));
|
||||||
const clusterStackHeight = Math.max(RESULT_CLUSTER_ROW_HEIGHT, minimizedWindows.length * RESULT_CLUSTER_ROW_HEIGHT);
|
const clusterStackHeight = Math.max(RESULT_CLUSTER_ROW_HEIGHT, minimizedWindows.length * RESULT_CLUSTER_ROW_HEIGHT);
|
||||||
const maxClusterLeft = Math.max(0, viewportSize.width - effectiveClusterWidth);
|
const maxClusterLeft = Math.max(0, viewportSize.width - effectiveClusterWidth);
|
||||||
|
|
@ -444,6 +447,8 @@ export function DatasetViewer() {
|
||||||
clusterCount={clusteredWindows.length}
|
clusterCount={clusteredWindows.length}
|
||||||
clusterPosition={effectiveClusterPos}
|
clusterPosition={effectiveClusterPos}
|
||||||
clusterWidth={effectiveClusterWidth}
|
clusterWidth={effectiveClusterWidth}
|
||||||
|
clusterLayer={clusterLayer}
|
||||||
|
onActivateCluster={activateCluster}
|
||||||
isClusterTop={isClusterTop}
|
isClusterTop={isClusterTop}
|
||||||
onMoveCluster={(dx, dy) => setClusterPos(current => {
|
onMoveCluster={(dx, dy) => setClusterPos(current => {
|
||||||
const cur = current ?? defaultClusterPos;
|
const cur = current ?? defaultClusterPos;
|
||||||
|
|
|
||||||
|
|
@ -170,10 +170,14 @@ export function resultLead(value?: string): string | undefined {
|
||||||
for (const line of value.split(/\r?\n/)) {
|
for (const line of value.split(/\r?\n/)) {
|
||||||
// Exports that lost their line breaks keep the heading, table or next heading on the same line;
|
// Exports that lost their line breaks keep the heading, table or next heading on the same line;
|
||||||
// the statement ends where such a structure begins.
|
// the statement ends where such a structure begins.
|
||||||
|
// A list ends the statement as well: at the start of a line, or flattened behind its introducing colon
|
||||||
|
// ("excluded: - [Policy](…)"). A marker inside a sentence ("access * user") is prose.
|
||||||
const trimmed = line.trim().replace(/^#{1,6}\s+Test result\s+/i, '');
|
const trimmed = line.trim().replace(/^#{1,6}\s+Test result\s+/i, '');
|
||||||
const structure = /(?:^|\s)(?:\|\s|#{1,6}\s)/.exec(trimmed);
|
const structure = /(?:^|\s)(?:\|\s|#{1,6}\s)|^(?:[-*+]|\d+[.)])\s|(?<=:)\s+(?:[-*+]|\d+[.)])\s/.exec(trimmed);
|
||||||
const text = (structure ? trimmed.slice(0, structure.index) : trimmed).trim();
|
const text = (structure ? trimmed.slice(0, structure.index) : trimmed).trim();
|
||||||
const structural = !text || /^[-*_]{3,}$/.test(text);
|
const structural = !text || /^[-*_]{3,}$/.test(text);
|
||||||
|
// A result that opens with a list has no prose statement; a later paragraph is not promoted to it.
|
||||||
|
if (!text && structure && /^(?:[-*+]|\d+[.)])\s/.test(trimmed) && !/^(?:[-*_]\s*){3,}$/.test(trimmed)) break;
|
||||||
if (structural) {
|
if (structural) {
|
||||||
if (lines.length) break;
|
if (lines.length) break;
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,9 @@ interface Props {
|
||||||
clusterPosition?: { left: number; top: number };
|
clusterPosition?: { left: number; top: number };
|
||||||
/** Gemeinsame Breite des minimierten Clusters */
|
/** Gemeinsame Breite des minimierten Clusters */
|
||||||
clusterWidth?: number;
|
clusterWidth?: number;
|
||||||
|
/** Shared activation layer: the open Result always covers its minimized siblings. */
|
||||||
|
clusterLayer?: number;
|
||||||
|
onActivateCluster?(): void;
|
||||||
/** Verschiebt den minimierten Cluster synchron */
|
/** Verschiebt den minimierten Cluster synchron */
|
||||||
isClusterTop?: boolean;
|
isClusterTop?: boolean;
|
||||||
onMoveCluster?: (dx: number, dy: number) => void;
|
onMoveCluster?: (dx: number, dy: number) => void;
|
||||||
|
|
@ -124,7 +127,7 @@ export function FloatingResultsWindow({
|
||||||
id, title, sheets, toolId, outcome, finding, sections = [], color, zIndex, offset, search, onActivate, onClose,
|
id, title, sheets, toolId, outcome, finding, sections = [], color, zIndex, offset, search, onActivate, onClose,
|
||||||
onOpenRecord, actionLabel = 'To Report', actionTitle = 'Jump to this finding’s record',
|
onOpenRecord, actionLabel = 'To Report', actionTitle = 'Jump to this finding’s record',
|
||||||
minimized = false, dimmed = false, onToggleMinimize, detached = false, onDetach, clusterIndex, isClusterBottom = false, clusterCount = 0,
|
minimized = false, dimmed = false, onToggleMinimize, detached = false, onDetach, clusterIndex, isClusterBottom = false, clusterCount = 0,
|
||||||
clusterPosition, clusterWidth, isClusterTop = false, onMoveCluster, onResizeClusterWidth,
|
clusterPosition, clusterWidth, clusterLayer, onActivateCluster, isClusterTop = false, onMoveCluster, onResizeClusterWidth,
|
||||||
bookmarked = false, onToggleBookmark, allClusterBookmarked = false, onBookmarkCluster, onCloseCluster, onDockCluster
|
bookmarked = false, onToggleBookmark, allClusterBookmarked = false, onBookmarkCluster, onCloseCluster, onDockCluster
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const titleParts = splitTitle(title);
|
const titleParts = splitTitle(title);
|
||||||
|
|
@ -178,6 +181,7 @@ export function FloatingResultsWindow({
|
||||||
* herausgezogenes Result nicht unbeabsichtigt wieder clustern koennen.
|
* herausgezogenes Result nicht unbeabsichtigt wieder clustern koennen.
|
||||||
*/
|
*/
|
||||||
const isDetached = !minimized && detached;
|
const isDetached = !minimized && detached;
|
||||||
|
useEffect(() => { if (isDetached) activate(); }, [isDetached]);
|
||||||
/**
|
/**
|
||||||
* Das Fenster oeffnet mittig. Die erste Schaetzung nutzt die Hoechstmasse aus
|
* Das Fenster oeffnet mittig. Die erste Schaetzung nutzt die Hoechstmasse aus
|
||||||
* dem Stil, weil das Element noch nicht gemessen werden kann; unmittelbar
|
* dem Stil, weil das Element noch nicht gemessen werden kann; unmittelbar
|
||||||
|
|
@ -386,6 +390,8 @@ export function FloatingResultsWindow({
|
||||||
};
|
};
|
||||||
|
|
||||||
const isStackedCluster = !maximized && !isDetached && clusterPosition !== undefined && clusterIndex !== undefined;
|
const isStackedCluster = !maximized && !isDetached && clusterPosition !== undefined && clusterIndex !== undefined;
|
||||||
|
const usesClusterLayer = !isDetached && clusterPosition !== undefined && clusterLayer !== undefined;
|
||||||
|
const activateLayer = usesClusterLayer && onActivateCluster ? onActivateCluster : activate;
|
||||||
// Spaltenbreiten gehoeren zur frei bearbeitbaren Tabellenflaeche. Im
|
// Spaltenbreiten gehoeren zur frei bearbeitbaren Tabellenflaeche. Im
|
||||||
// Findings-Cluster bleibt das aktive Result dagegen bewusst unveraendert,
|
// Findings-Cluster bleibt das aktive Result dagegen bewusst unveraendert,
|
||||||
// damit die schmale, mit den minimierten Slots geteilte Geometrie stabil
|
// damit die schmale, mit den minimierten Slots geteilte Geometrie stabil
|
||||||
|
|
@ -432,14 +438,14 @@ export function FloatingResultsWindow({
|
||||||
...(clusterCoverHeight > 0 ? { minHeight: clusterCoverHeight } : {}),
|
...(clusterCoverHeight > 0 ? { minHeight: clusterCoverHeight } : {}),
|
||||||
maxWidth: maximized ? maximizedWidth : availableWidth,
|
maxWidth: maximized ? maximizedWidth : availableWidth,
|
||||||
maxHeight: maximized ? maximizedHeight : Math.max(availableHeight, clusterCoverHeight),
|
maxHeight: maximized ? maximizedHeight : Math.max(availableHeight, clusterCoverHeight),
|
||||||
zIndex: layer,
|
zIndex: usesClusterLayer ? clusterLayer! + (minimized ? 0 : 1) : layer,
|
||||||
'--result-color': color
|
'--result-color': color
|
||||||
} as unknown as CSSProperties;
|
} as unknown as CSSProperties;
|
||||||
|
|
||||||
// Der Fundname steht zusaetzlich zur Kennzeichnung in der Kopfleiste, damit
|
// Der Fundname steht zusaetzlich zur Kennzeichnung in der Kopfleiste, damit
|
||||||
// er auch im minimierten Zustand (Rumpf ausgeblendet) sichtbar bleibt.
|
// er auch im minimierten Zustand (Rumpf ausgeblendet) sichtbar bleibt.
|
||||||
return (
|
return (
|
||||||
<section ref={windowRef} className={`results-window${minimized ? ' is-minimized' : ''}${maximized ? ' is-maximized' : ''}${isClusterBottom ? ' is-cluster-bottom' : ''}${dimmed ? ' is-dimmed' : ''}`} style={style} role="dialog" aria-modal="false" aria-label={title} onPointerDownCapture={activate} onFocusCapture={activate} onPointerDown={onActivate}>
|
<section ref={windowRef} className={`results-window${minimized ? ' is-minimized' : ''}${maximized ? ' is-maximized' : ''}${isClusterBottom ? ' is-cluster-bottom' : ''}${dimmed ? ' is-dimmed' : ''}`} style={style} role="dialog" aria-modal="false" aria-label={title} onPointerDownCapture={activateLayer} onFocusCapture={activateLayer} onPointerDown={onActivate}>
|
||||||
{isClusterTop && (
|
{isClusterTop && (
|
||||||
<div className="results-cluster-bar" onPointerDown={startDrag} onPointerMove={moveDrag} onPointerUp={stopDrag} onPointerCancel={stopDrag}>
|
<div className="results-cluster-bar" onPointerDown={startDrag} onPointerMove={moveDrag} onPointerUp={stopDrag} onPointerCancel={stopDrag}>
|
||||||
<button className="results-cluster-slot" type="button" onClick={onDockCluster} disabled={!onDockCluster} title="Results in die Filterzeile einklappen" aria-label="Results in die Filterzeile einklappen" aria-expanded="true"><ClusterSlotIcon /></button>
|
<button className="results-cluster-slot" type="button" onClick={onDockCluster} disabled={!onDockCluster} title="Results in die Filterzeile einklappen" aria-label="Results in die Filterzeile einklappen" aria-expanded="true"><ClusterSlotIcon /></button>
|
||||||
|
|
|
||||||
|
|
@ -13,25 +13,62 @@ import policy from '../styles/policy-analysis.css?raw';
|
||||||
import scoring from '../styles/security-scoring.css?raw';
|
import scoring from '../styles/security-scoring.css?raw';
|
||||||
import icons from '../styles/functional-icons.css?raw';
|
import icons from '../styles/functional-icons.css?raw';
|
||||||
|
|
||||||
export const SECURITY_VIEW_SNAPSHOT_VERSION = '1.1.0';
|
export const SECURITY_VIEW_SNAPSHOT_VERSION = '1.2.0';
|
||||||
export const SECURITY_HTML_RENDERER_VERSION = '1.3.0';
|
export const SECURITY_HTML_RENDERER_VERSION = '1.4.2';
|
||||||
|
|
||||||
/** Declared Core Presentation read port; the Report Engine receives only an authorized frozen view. */
|
/** Declared Core Presentation read port; the Report Engine receives only an authorized frozen view. */
|
||||||
export interface SecurityViewReadPort {
|
export interface SecurityViewReadPort {
|
||||||
read(): Readonly<{
|
read(): Readonly<{
|
||||||
markup: string; popups?: string; width: number; theme: 'light' | 'dark'; density?: string;
|
markup: string; popups?: string; environmentMarkup?: string; width: number; theme: 'light' | 'dark'; density?: string;
|
||||||
client: string; assessments: string; generatedAt: string;
|
client: string; assessments: string; generatedAt: string;
|
||||||
|
assessmentDetails?: ReadonlyArray<Readonly<{ name: string; effectiveAt?: string }>>;
|
||||||
|
distribution?: Readonly<{ total: number; domains: ReadonlyArray<Readonly<{ label: string; count: number }>>;
|
||||||
|
categories: ReadonlyArray<Readonly<{ label: string; count: number }>> }>;
|
||||||
versions: ReadonlyArray<readonly [string, string]>;
|
versions: ReadonlyArray<readonly [string, string]>;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assessmentHeading(name: string, effectiveAt?: string): string {
|
||||||
|
const embedded = /^Assessment\s+(\d{4}-\d{2}-\d{2})$/i.exec(name.trim());
|
||||||
|
const sourceDate = effectiveAt || embedded?.[1];
|
||||||
|
const parsed = sourceDate && /^\d{4}-\d{2}-\d{2}(?:T.*)?$/.test(sourceDate) ? new Date(sourceDate) : undefined;
|
||||||
|
const date = parsed && Number.isFinite(parsed.getTime()) && (!/^\d{4}-\d{2}-\d{2}$/.test(sourceDate!) || parsed.toISOString().slice(0, 10) === sourceDate)
|
||||||
|
? new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: 'Europe/Berlin' }).format(parsed) : undefined;
|
||||||
|
const heading = embedded || /^Assessment$/i.test(name.trim()) ? 'Assessment' : `Assessment: ${name}`;
|
||||||
|
return `${heading}${date ? ' · ' + date : ' · Datum nicht verfügbar'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function distributionParagraph(distribution: ReturnType<SecurityViewReadPort['read']>['distribution']): string {
|
||||||
|
if (!distribution) return '';
|
||||||
|
const { total, domains, categories } = distribution;
|
||||||
|
if (!Number.isSafeInteger(total) || total < 0 || [...domains, ...categories].some(item => !Number.isSafeInteger(item.count) || item.count < 0)
|
||||||
|
|| domains.reduce((sum, item) => sum + item.count, 0) !== total || categories.reduce((sum, item) => sum + item.count, 0) !== total) {
|
||||||
|
throw new Error('The Security distribution does not reconcile to the authoritative deviation total.');
|
||||||
|
}
|
||||||
|
if (!total) return '<p class="security-export-summary">Im ausgewählten Assessment sind keine Abweichungen ausgewiesen.</p>';
|
||||||
|
const number = (value: number) => value.toLocaleString('de-DE');
|
||||||
|
const populatedDomains = domains.filter(item => item.count > 0);
|
||||||
|
const populatedCategories = categories.filter(item => item.count > 0);
|
||||||
|
const largest = (items: typeof domains) => [...items].sort((left, right) => right.count - left.count)[0];
|
||||||
|
const domain = largest(populatedDomains), category = largest(populatedCategories);
|
||||||
|
return `<p class="security-export-summary">Die <strong>${number(total)}</strong> Abweichungen verteilen sich auf <strong>${number(populatedDomains.length)}</strong> Security ${populatedDomains.length === 1 ? 'Domain' : 'Domains'} und <strong>${number(populatedCategories.length)}</strong> ${populatedCategories.length === 1 ? 'Problemfeld' : 'Problemfelder'}. Den größten Anteil trägt <strong>${escapeHtml(domain.label)}</strong> (${number(domain.count)}, ${number(Math.round(100 * domain.count / total))} %); das am stärksten belegte Problemfeld ist <strong>${escapeHtml(category.label)}</strong> (${number(category.count)}).</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildSecurityViewReportHtml(port: SecurityViewReadPort): string {
|
export function buildSecurityViewReportHtml(port: SecurityViewReadPort): string {
|
||||||
const snapshot = port.read();
|
const snapshot = port.read();
|
||||||
if (!Number.isFinite(snapshot.width) || snapshot.width <= 0) throw new Error('The Security view has no measured width.');
|
if (!Number.isFinite(snapshot.width) || snapshot.width <= 0) throw new Error('The Security view has no measured width.');
|
||||||
if (/<(?:script|iframe|object|embed|form)\b|<[^>]*\son[a-z]+\s*=|(?:href|src)\s*=\s*["']\s*javascript:/i.test(snapshot.markup + (snapshot.popups ?? ''))) throw new Error('The Security view contains unsupported active content.');
|
if (/<(?:script|iframe|object|embed|form)\b|<[^>]*\son[a-z]+\s*=|(?:href|src)\s*=\s*["']\s*javascript:/i.test(snapshot.markup + (snapshot.popups ?? '') + (snapshot.environmentMarkup ?? ''))) throw new Error('The Security view contains unsupported active content.');
|
||||||
const esc = escapeHtml;
|
const esc = escapeHtml;
|
||||||
const density = ['compact', 'reading'].includes(snapshot.density ?? '') ? snapshot.density : '';
|
const density = ['compact', 'reading'].includes(snapshot.density ?? '') ? snapshot.density : '';
|
||||||
const pairs = [['ASSESSOR', ASSESSOR_PRODUCT_VERSION], ...snapshot.versions, ['Security snapshot / Renderer', `${SECURITY_VIEW_SNAPSHOT_VERSION} / ${SECURITY_HTML_RENDERER_VERSION}`]];
|
const pairs = [['ASSESSOR', ASSESSOR_PRODUCT_VERSION], ...snapshot.versions, ['Security snapshot / Renderer', `${SECURITY_VIEW_SNAPSHOT_VERSION} / ${SECURITY_HTML_RENDERER_VERSION}`]];
|
||||||
|
const assessmentLines = snapshot.assessmentDetails?.length
|
||||||
|
? snapshot.assessmentDetails.map(item => assessmentHeading(item.name, item.effectiveAt)) : [snapshot.assessments];
|
||||||
|
// Native radios select the same complete shared theme declarations, including standalone popups.
|
||||||
|
const themeBlock = (theme: 'dark' | 'light') => new RegExp(':root\\[data-theme="' + theme + '"\\]\\s*\\{([\\s\\S]*?)\\n\\}').exec(uiTokens)?.[1];
|
||||||
|
const darkTheme = themeBlock('dark'), lightTheme = themeBlock('light');
|
||||||
|
const proseSize = /--type-table-body-size:\s*([^;]+);/.exec(uiTokens)?.[1];
|
||||||
|
if (!darkTheme || !lightTheme || !proseSize) throw new Error('Shared Security report theme or typography roles are unavailable.');
|
||||||
|
const themeCss = `@layer tokens{:root:has(#security-theme-dark:checked){${darkTheme}}:root:has(#security-theme-light:checked){${lightTheme}}@media(prefers-color-scheme:dark){:root:has(#security-theme-system:checked){${darkTheme}}}@media(prefers-color-scheme:light){:root:has(#security-theme-system:checked){${lightTheme}}}}`;
|
||||||
// A fragment inside native details reveals its ancestors without executable report content.
|
// A fragment inside native details reveals its ancestors without executable report content.
|
||||||
const markup = snapshot.markup.replace(/href="#security-category-([\w-]+)"/g, 'href="#security-category-content-$1"')
|
const markup = snapshot.markup.replace(/href="#security-category-([\w-]+)"/g, 'href="#security-category-content-$1"')
|
||||||
.replace(/(<details\b[^>]*\bid="security-category-([\w-]+)"[^>]*>)([\s\S]*?)(<div class="client-security-group-content")/g,
|
.replace(/(<details\b[^>]*\bid="security-category-([\w-]+)"[^>]*>)([\s\S]*?)(<div class="client-security-group-content")/g,
|
||||||
|
|
@ -41,17 +78,24 @@ export function buildSecurityViewReportHtml(port: SecurityViewReadPort): string
|
||||||
return `<!doctype html><html lang="de" data-theme="${snapshot.theme === 'dark' ? 'dark' : 'light'}" data-density="${density}"><head>
|
return `<!doctype html><html lang="de" data-theme="${snapshot.theme === 'dark' ? 'dark' : 'light'}" data-density="${density}"><head>
|
||||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data:; font-src data:; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'">
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data:; font-src data:; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'">
|
||||||
<title>Security Export · ${esc(snapshot.client)} · VERTRAULICH</title><style>
|
<title>Security Report · ${esc(snapshot.client)} · VERTRAULICH</title><style>
|
||||||
@font-face{font-family:'Inter Variable';src:url('${REPORT_INTER_FONT}') format('woff2');font-weight:100 900;font-display:swap}
|
@font-face{font-family:'Inter Variable';src:url('${REPORT_INTER_FONT}') format('woff2');font-weight:100 900;font-display:swap}
|
||||||
${uiTokens}\n${tokens}\n${base}\n${components}\n${datahub}\n${security}\n${policy}\n${scoring}\n${icons}
|
${uiTokens}\n${tokens}\n${base}\n${components}\n${datahub}\n${security}\n${policy}\n${scoring}\n${icons}
|
||||||
|
${themeCss}
|
||||||
|
:root{--type-table-body-size:var(--type-label-size);--type-body-size:${proseSize};--type-h3-size:var(--type-h4-size)}
|
||||||
html,body{background:var(--surface-panel)}
|
html,body{background:var(--surface-panel)}
|
||||||
body{overflow:auto;min-height:100vh;font-size:var(--type-table-body-size)}
|
body{overflow:auto;min-height:100vh;font-size:var(--type-table-body-size)}
|
||||||
.security-export{width:min(${snapshot.width}px,calc(100vw * 2 / 3));max-width:100%;margin:0 auto;color:var(--text-primary);background:var(--surface-panel);container:security-report / inline-size}
|
.security-export{width:min(${snapshot.width}px,calc(100vw / 2));max-width:100%;margin:0 auto;color:var(--text-primary);background:var(--surface-panel);container:security-report / inline-size}
|
||||||
.security-export .client-security-group:has(>.client-security-group-content:target)>summary{background:var(--surface-selected);border-inline-start:var(--marker-width) solid var(--marking-color)}
|
.security-export .client-security-group:has(>.client-security-group-content:target)>summary{background:var(--surface-selected);border-inline-start:var(--marker-width) solid var(--marking-color)}
|
||||||
.security-export-head,.security-export-results{padding:var(--panel-inset)}
|
.security-export-head,.security-export-results{padding:var(--panel-inset)}
|
||||||
.security-export-brand{display:flex;align-items:center;gap:var(--space-4)}
|
.security-export-brand{display:flex;align-items:center;gap:var(--space-4);flex-wrap:wrap}
|
||||||
.security-export-brand img{width:var(--space-8);height:var(--space-8)}
|
.security-export-brand img{width:var(--space-8);height:var(--space-8)}
|
||||||
.security-export-brand b{font-size:var(--type-h2-size)}
|
.security-export-brand b{font-size:var(--type-h2-size)}
|
||||||
|
.security-export-theme{display:flex;gap:var(--space-1);margin-left:auto}
|
||||||
|
.security-export-theme label{position:relative;display:inline-flex;align-items:center;justify-content:center;min-width:var(--btn-h);cursor:pointer}
|
||||||
|
.security-export-theme input{position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer}
|
||||||
|
.security-export-theme label:has(input:checked){background:var(--button-accent);color:var(--button-accent-text)}
|
||||||
|
.security-export-theme label:focus-within{outline:var(--bw-emphasis) solid var(--focus-ring);outline-offset:var(--space-1)}
|
||||||
.security-export-confidential{margin-left:auto;padding:var(--space-2) var(--space-4);border:var(--bw-emphasis) solid var(--text-primary);font-size:var(--type-h2-size);font-weight:var(--fw-bold);letter-spacing:var(--ls-caps)}
|
.security-export-confidential{margin-left:auto;padding:var(--space-2) var(--space-4);border:var(--bw-emphasis) solid var(--text-primary);font-size:var(--type-h2-size);font-weight:var(--fw-bold);letter-spacing:var(--ls-caps)}
|
||||||
.security-export-head h1{margin:var(--space-4) 0 var(--space-2);font-size:var(--type-h2-size)}
|
.security-export-head h1{margin:var(--space-4) 0 var(--space-2);font-size:var(--type-h2-size)}
|
||||||
.security-export-head h2{font-size:var(--type-h4-size)}
|
.security-export-head h2{font-size:var(--type-h4-size)}
|
||||||
|
|
@ -61,6 +105,16 @@ body{overflow:auto;min-height:100vh;font-size:var(--type-table-body-size)}
|
||||||
.security-export-versions dd{margin:0;font-size:var(--type-body-size)}
|
.security-export-versions dd{margin:0;font-size:var(--type-body-size)}
|
||||||
.security-export .client-security-panel{height:auto;min-height:0;overflow:visible;background:var(--surface-panel)}
|
.security-export .client-security-panel{height:auto;min-height:0;overflow:visible;background:var(--surface-panel)}
|
||||||
.security-export .client-security-table{background:var(--surface-panel)}
|
.security-export .client-security-table{background:var(--surface-panel)}
|
||||||
|
.security-export-environment{padding:var(--panel-inset);border-block:var(--border-hair) solid var(--border-subtle)}
|
||||||
|
.security-export-environment>h2{margin:0 0 var(--space-4);font-size:var(--type-h4-size)}
|
||||||
|
.security-export-environment .client-security-hardfacts{padding-inline-start:0;transform:none}
|
||||||
|
.security-export-environment .client-security-fact-groups{gap:var(--space-5)}
|
||||||
|
.security-export-environment .client-security-fact-column{gap:var(--space-1)}
|
||||||
|
.security-export-environment .client-env-group-header{min-height:var(--btn-h);align-items:center}
|
||||||
|
.security-export-environment .client-security-facts>div{box-sizing:border-box;line-height:calc(var(--type-body-size) * 1.2);min-height:calc(var(--type-body-size) * 1.2 + var(--space-1) + var(--border-hair))}
|
||||||
|
.security-export-environment .client-environment-results-button{height:var(--btn-h);min-height:var(--btn-h);border-radius:var(--btn-radius);font-size:var(--type-button-size)}
|
||||||
|
.security-export-summary{font-size:var(--type-body-size);line-height:var(--lh-prose);max-width:85ch}
|
||||||
|
.security-export .client-security-overview{grid-template-columns:minmax(0,1fr)}
|
||||||
.security-export .client-security-map-scroll{overflow-x:auto}
|
.security-export .client-security-map-scroll{overflow-x:auto}
|
||||||
.security-export .client-security-map-scroll>svg{width:1280px;max-width:none}
|
.security-export .client-security-map-scroll>svg{width:1280px;max-width:none}
|
||||||
@container security-report (max-width:1800px){.security-export .client-security-overview{grid-template-columns:minmax(0,1fr)}.security-export .client-security-hardfacts{transform:none;padding-inline-start:0;align-self:start}.security-export .client-security-hardfacts-content{max-width:1280px}}
|
@container security-report (max-width:1800px){.security-export .client-security-overview{grid-template-columns:minmax(0,1fr)}.security-export .client-security-hardfacts{transform:none;padding-inline-start:0;align-self:start}.security-export .client-security-hardfacts-content{max-width:1280px}}
|
||||||
|
|
@ -87,8 +141,9 @@ body{overflow:auto;min-height:100vh;font-size:var(--type-table-body-size)}
|
||||||
.security-export-maximize:has(input:checked) .restore-label{display:inline}
|
.security-export-maximize:has(input:checked) .restore-label{display:inline}
|
||||||
.security-export-popup:has(>.security-export-popup-body){overscroll-behavior:contain}
|
.security-export-popup:has(>.security-export-popup-body){overscroll-behavior:contain}
|
||||||
.security-export-popup:has(>header input:checked){width:calc(100vw - var(--space-8));height:calc(100dvh - var(--space-8));resize:none}
|
.security-export-popup:has(>header input:checked){width:calc(100vw - var(--space-8));height:calc(100dvh - var(--space-8));resize:none}
|
||||||
.security-export-popup-body{overflow:auto;overscroll-behavior:contain;min-height:0;padding:var(--panel-inset);font-size:var(--type-table-body-size)}
|
.security-export-popup-body{overflow:auto;overscroll-behavior:contain;min-height:0;padding:var(--panel-inset);font-size:var(--type-body-size)}
|
||||||
.security-export-popup .security-export-popup-body p,.security-export-popup .security-export-popup-body dd,.security-export-popup .security-export-popup-body .client-env-evidence-empty,.security-export-popup .security-export-popup-body .security-scoring-methodology caption{font-size:var(--type-table-body-size)}
|
.security-export-popup .security-export-popup-body p,.security-export-popup .security-export-popup-body .client-env-evidence-empty,.security-export-popup .security-export-popup-body .security-scoring-methodology caption{font-size:var(--type-body-size)}
|
||||||
|
.security-export-popup .security-export-popup-body dd{font-size:var(--type-table-body-size)}
|
||||||
.security-export-popup-body>dl{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:0;margin:0 0 var(--space-4);font-size:var(--type-table-body-size)}
|
.security-export-popup-body>dl{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:0;margin:0 0 var(--space-4);font-size:var(--type-table-body-size)}
|
||||||
.security-export-popup-body>dl>div{min-width:0;padding:var(--space-2);border:var(--border-hair) solid var(--border-subtle)}
|
.security-export-popup-body>dl>div{min-width:0;padding:var(--space-2);border:var(--border-hair) solid var(--border-subtle)}
|
||||||
.security-export-popup-body>dl dt{color:var(--text-secondary);font-weight:var(--fw-regular)}
|
.security-export-popup-body>dl dt{color:var(--text-secondary);font-weight:var(--fw-regular)}
|
||||||
|
|
@ -96,13 +151,15 @@ body{overflow:auto;min-height:100vh;font-size:var(--type-table-body-size)}
|
||||||
.security-export-popup .client-env-evidence-scroll{max-height:none}
|
.security-export-popup .client-env-evidence-scroll{max-height:none}
|
||||||
@media(max-width:900px){.security-export{width:calc(100vw - var(--space-8))}.security-export-popup-body>dl{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
@media(max-width:900px){.security-export{width:calc(100vw - var(--space-8))}.security-export-popup-body>dl{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
||||||
@media(max-width:600px){.security-export-popup-body>dl{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
@media(max-width:600px){.security-export-popup-body>dl{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||||
@media print{.security-export-maximize{display:none}}
|
@media print{.security-export-maximize,.security-export-theme{display:none}}
|
||||||
@media print{body{overflow:visible}.security-export{margin:0}.security-export-confidential{-webkit-print-color-adjust:exact;print-color-adjust:exact}details::details-content{content-visibility:visible;display:block}summary{break-after:avoid}tr,.client-security-finding-detail{break-inside:avoid}.security-export-popup{display:block;position:static;max-height:none;box-shadow:none;margin:var(--space-5) 0}.security-export-popup>header button{display:none}.security-export-popup-body{overflow:visible}}
|
@media print{body{overflow:visible}.security-export{margin:0}.security-export-confidential{-webkit-print-color-adjust:exact;print-color-adjust:exact}details::details-content{content-visibility:visible;display:block}summary{break-after:avoid}tr,.client-security-finding-detail{break-inside:avoid}.security-export-popup{display:block;position:static;max-height:none;box-shadow:none;margin:var(--space-5) 0}.security-export-popup>header button{display:none}.security-export-popup-body{overflow:visible}}
|
||||||
</style></head><body><main class="security-export">
|
</style></head><body><main class="security-export">
|
||||||
<header class="security-export-head"><div class="security-export-brand"><img src="${REPORT_ASSESSOR_LOGO}" alt="ASSESSOR Logo"><div><b>ASSESSOR</b><div>${esc(ASSESSOR_PRODUCT_VERSION)}</div></div><strong class="security-export-confidential">VERTRAULICH</strong></div>
|
<header class="security-export-head"><div class="security-export-brand"><img src="${REPORT_ASSESSOR_LOGO}" alt="ASSESSOR Logo"><div><b>ASSESSOR</b><div>${esc(ASSESSOR_PRODUCT_VERSION)}</div></div><div class="security-export-theme" role="radiogroup" aria-label="Report color scheme">${(['dark', 'system', 'light'] as const).map((theme, index) => `<label class="btn" title="${['Dark Mode', 'System mode', 'Light Mode'][index]}"><input type="radio" name="security-report-theme" id="security-theme-${theme}" value="${theme}" aria-label="${['Dark Mode', 'System mode', 'Light Mode'][index]}"${snapshot.theme === theme ? ' checked' : ''}><span aria-hidden="true">${['◔', '▣', '☀'][index]}</span></label>`).join('')}</div><strong class="security-export-confidential">VERTRAULICH</strong></div>
|
||||||
<h1>Security Export</h1><p>${esc(snapshot.client)} · ${esc(snapshot.assessments)}</p><h2>Datenstand und Versionen</h2>
|
<h1>Security Report</h1><p class="security-export-client">${esc(snapshot.client)}</p>${assessmentLines.map(line => `<p class="security-export-assessment">${esc(line)}</p>`).join('')}<h2>Datenstand und Versionen</h2>
|
||||||
<dl class="security-export-versions">${pairs.map(([label, value]) => `<div><dt>${esc(label)}</dt><dd>${esc(value)}</dd></div>`).join('')}</dl>
|
<dl class="security-export-versions">${pairs.map(([label, value]) => `<div><dt>${esc(label)}</dt><dd>${esc(value)}</dd></div>`).join('')}</dl>
|
||||||
<p>Vollständige Security-Ansicht · Schriftgrößen wie im Security-Panel. Kategorien und Findings sind aufklappbar; Results und Originaltabellen öffnen als Popups.</p></header>
|
${distributionParagraph(snapshot.distribution)}
|
||||||
|
</header>
|
||||||
|
${snapshot.environmentMarkup ? `<section class="security-export-environment" aria-labelledby="security-export-environment-title"><h2 id="security-export-environment-title">System- und Umgebungsübersicht</h2>${snapshot.environmentMarkup}</section>` : ''}
|
||||||
${markup}<footer class="security-export-foot">VERTRAULICH · ${esc(snapshot.client)} · ASSESSOR ${esc(ASSESSOR_PRODUCT_VERSION)}</footer>
|
${markup}<footer class="security-export-foot">VERTRAULICH · ${esc(snapshot.client)} · ASSESSOR ${esc(ASSESSOR_PRODUCT_VERSION)}</footer>
|
||||||
</main>${popups}</body></html>`;
|
</main>${popups}</body></html>`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const components = readFileSync(new URL('../../src/styles/components.css', impor
|
||||||
|
|
||||||
describe('ASSESSOR version in the brand header', () => {
|
describe('ASSESSOR version in the brand header', () => {
|
||||||
it('shows the centrally maintained product version and opens About from the brand', () => {
|
it('shows the centrally maintained product version and opens About from the brand', () => {
|
||||||
expect(ASSESSOR_PRODUCT_VERSION).toBe('2.25.2');
|
expect(ASSESSOR_PRODUCT_VERSION).toBe('2.25.6');
|
||||||
expect(app).toContain("'db-structure': 'Data Flow'");
|
expect(app).toContain("'db-structure': 'Data Flow'");
|
||||||
expect(app).toContain("['db-structure', 'Data Flow']");
|
expect(app).toContain("['db-structure', 'Data Flow']");
|
||||||
expect(app).not.toContain("'DB Structure'");
|
expect(app).not.toContain("'DB Structure'");
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ describe('context engine', () => {
|
||||||
|
|
||||||
it('derives declared facts per GPO backup and aggregates conditions without assuming precedence', () => {
|
it('derives declared facts per GPO backup and aggregates conditions without assuming precedence', () => {
|
||||||
const result = context(declared);
|
const result = context(declared);
|
||||||
expect(result.engine.version).toBe('1.12.0');
|
expect(result.engine.version).toBe('1.13.0');
|
||||||
expect(result.coverage).toEqual({ observations: 8, semanticallyMapped: 6, notSemanticallyMapped: 1, unresolvedIdentity: 1,
|
expect(result.coverage).toEqual({ observations: 8, semanticallyMapped: 6, notSemanticallyMapped: 1, unresolvedIdentity: 1,
|
||||||
scannerFindings: 0, scannerMapped: 0, scannerNotInCatalog: 0, scannerGuardRejected: 0, gpoEvaluatedSettings: 0, gpoMappedByVerdict: 0, gpoUncatalogued: 0, scannerMappedBySection: 0, scannerUncatalogued: 0, scannerWithDeclaredReferences: 0, scannerWithCisControlArea: 0,
|
scannerFindings: 0, scannerMapped: 0, scannerNotInCatalog: 0, scannerGuardRejected: 0, gpoEvaluatedSettings: 0, gpoMappedByVerdict: 0, gpoUncatalogued: 0, scannerMappedBySection: 0, scannerUncatalogued: 0, scannerWithDeclaredReferences: 0, scannerWithCisControlArea: 0,
|
||||||
scannerMappedByDeclaredTechnique: 0, scannerRecognized: 0, scannerEstimated: 0, gpoRecognized: 0, gpoEstimated: 0, uncataloguedWithoutRecognition: 0 });
|
scannerMappedByDeclaredTechnique: 0, scannerRecognized: 0, scannerEstimated: 0, gpoRecognized: 0, gpoEstimated: 0, uncataloguedWithoutRecognition: 0 });
|
||||||
|
|
@ -113,7 +113,7 @@ describe('context engine', () => {
|
||||||
expect(condition(result, 'ldap-client-signing-none').unknownBy).toHaveLength(1);
|
expect(condition(result, 'ldap-client-signing-none').unknownBy).toHaveLength(1);
|
||||||
expect(condition(result, 'ldap-channel-binding-not-always')).toMatchObject({ state: 'unknown', satisfiedBy: [], unknownBy: [] });
|
expect(condition(result, 'ldap-channel-binding-not-always')).toMatchObject({ state: 'unknown', satisfiedBy: [], unknownBy: [] });
|
||||||
|
|
||||||
expect(result.clusters[0]).toMatchObject({ clusterId: 'credential-relay-exposure', state: 'present', satisfied: 4, notSatisfied: 1, unknown: 21 });
|
expect(result.clusters[0]).toMatchObject({ clusterId: 'credential-relay-exposure', state: 'present', satisfied: 4, notSatisfied: 1, unknown: 26 });
|
||||||
expect(JSON.stringify(result)).not.toMatch(/"(?:verdict|pass|fail|severity|priority|rank)"/i);
|
expect(JSON.stringify(result)).not.toMatch(/"(?:verdict|pass|fail|severity|priority|rank)"/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -145,7 +145,7 @@ describe('context engine', () => {
|
||||||
data: { kind: 'uint32', value: hardenedValue(semantic) } }]
|
data: { kind: 'uint32', value: hardenedValue(semantic) } }]
|
||||||
}))));
|
}))));
|
||||||
// Conditions without a GPO setting (spooler coercion, ESC8, name resolution, hardened paths, SMB, DNS, TLS, report sections) stay unknown.
|
// Conditions without a GPO setting (spooler coercion, ESC8, name resolution, hardened paths, SMB, DNS, TLS, report sections) stay unknown.
|
||||||
expect(hardened.clusters[0]).toMatchObject({ state: 'undetermined', satisfied: 0, notSatisfied: 8, unknown: 18 });
|
expect(hardened.clusters[0]).toMatchObject({ state: 'undetermined', satisfied: 0, notSatisfied: 8, unknown: 23 });
|
||||||
expect(hardened.levers).toEqual([]);
|
expect(hardened.levers).toEqual([]);
|
||||||
const { observations, analyses } = fixture(RELEASED_ASSESSMENT_CONTEXT_CATALOG.semantics.flatMap(semantic => semantic.targets.map((target, index) => ({
|
const { observations, analyses } = fixture(RELEASED_ASSESSMENT_CONTEXT_CATALOG.semantics.flatMap(semantic => semantic.targets.map((target, index) => ({
|
||||||
id: `obs-h-${semantic.id}-${index}`, backup: 'backup-a',
|
id: `obs-h-${semantic.id}-${index}`, backup: 'backup-a',
|
||||||
|
|
@ -169,7 +169,7 @@ describe('context engine', () => {
|
||||||
passing('cis-cat', '18.6.8.4', "18.6.8.4 Ensure 'Enable insecure guest logons' is set to 'Disabled'")]
|
passing('cis-cat', '18.6.8.4', "18.6.8.4 Ensure 'Enable insecure guest logons' is set to 'Disabled'")]
|
||||||
});
|
});
|
||||||
// Refuted conditions no longer count; report-section and newly catalogued conditions without evidence stay unknown.
|
// Refuted conditions no longer count; report-section and newly catalogued conditions without evidence stay unknown.
|
||||||
expect(fullyHardened.clusters[0]).toMatchObject({ state: 'undetermined', satisfied: 0, notSatisfied: 17, unknown: 9 });
|
expect(fullyHardened.clusters[0]).toMatchObject({ state: 'undetermined', satisfied: 0, notSatisfied: 17, unknown: 14 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is deterministic and never changes its inputs', () => {
|
it('is deterministic and never changes its inputs', () => {
|
||||||
|
|
@ -227,7 +227,7 @@ describe('context engine scanner observations', () => {
|
||||||
finding('cis-cat', '9.9.9', 'fail', 'occ-cis-lookalike', "9.9.9 Ensure 'Microsoft network client: Digitally sign communications (always)' is set to 'Enabled'"),
|
finding('cis-cat', '9.9.9', 'fail', 'occ-cis-lookalike', "9.9.9 Ensure 'Microsoft network client: Digitally sign communications (always)' is set to 'Enabled'"),
|
||||||
finding('pingcastle', 'S-OldNtlm', 'fail', 'occ-pc', 'The LAN Manager Authentication Level allows the use of NTLMv1 or LM.'),
|
finding('pingcastle', 'S-OldNtlm', 'fail', 'occ-pc', 'The LAN Manager Authentication Level allows the use of NTLMv1 or LM.'),
|
||||||
finding('cis-cat', '18.6.4.4', 'not_run', 'occ-cis-llmnr', "18.6.4.4 Ensure 'Turn off multicast name resolution' is set to 'Enabled'"),
|
finding('cis-cat', '18.6.4.4', 'not_run', 'occ-cis-llmnr', "18.6.4.4 Ensure 'Turn off multicast name resolution' is set to 'Enabled'"),
|
||||||
finding('maester', 'MT.1001', 'fail', 'occ-maester', 'Unrelated cloud check')
|
finding('maester', 'MT.0001', 'fail', 'occ-maester', 'Unrelated cloud check')
|
||||||
];
|
];
|
||||||
|
|
||||||
function withScanners(declared: Declared[]) {
|
function withScanners(declared: Declared[]) {
|
||||||
|
|
@ -344,8 +344,8 @@ describe('context catalog clusters', () => {
|
||||||
});
|
});
|
||||||
const cluster = (id: string) => result.clusters.find(item => item.clusterId === id)!;
|
const cluster = (id: string) => result.clusters.find(item => item.clusterId === id)!;
|
||||||
expect(cluster('cloud-privileged-access-exposure')).toMatchObject({ state: 'present', satisfied: 3, notSatisfied: 1 });
|
expect(cluster('cloud-privileged-access-exposure')).toMatchObject({ state: 'present', satisfied: 3, notSatisfied: 1 });
|
||||||
expect(cluster('ad-privileged-account-exposure')).toMatchObject({ state: 'undetermined', satisfied: 1, notSatisfied: 1, unknown: 7 });
|
expect(cluster('ad-privileged-account-exposure')).toMatchObject({ state: 'undetermined', satisfied: 1, notSatisfied: 1, unknown: 11 });
|
||||||
expect(cluster('cloud-sign-in-protection-gaps')).toMatchObject({ state: 'undetermined', satisfied: 0, unknown: 18 });
|
expect(cluster('cloud-sign-in-protection-gaps')).toMatchObject({ state: 'undetermined', satisfied: 0, unknown: 19 });
|
||||||
const permanent = cluster('cloud-privileged-access-exposure').conditions.find(item => item.conditionId === 'entra-permanent-privileged-assignments')!;
|
const permanent = cluster('cloud-privileged-access-exposure').conditions.find(item => item.conditionId === 'entra-permanent-privileged-assignments')!;
|
||||||
expect(permanent.semanticId).toBeUndefined();
|
expect(permanent.semanticId).toBeUndefined();
|
||||||
expect(permanent.relationships[0].techniqueIds).toEqual(['T1078.004']);
|
expect(permanent.relationships[0].techniqueIds).toEqual(['T1078.004']);
|
||||||
|
|
@ -433,7 +433,7 @@ describe('context engine CIS control areas', () => {
|
||||||
['9.2 Private Profile', 'x', '1', '0', '?'], ['9.1.1 Ensure synthetic setting', '0', '1', '0', '0%']]),
|
['9.2 Private Profile', 'x', '1', '0', '?'], ['9.1.1 Ensure synthetic setting', '0', '1', '0', '0%']]),
|
||||||
scores('run-client', [['9.1 Firewall: Domain', '3', '0', '0', '100%']])
|
scores('run-client', [['9.1 Firewall: Domain', '3', '0', '0', '100%']])
|
||||||
]);
|
]);
|
||||||
expect(result.engine.version).toBe('1.12.0');
|
expect(result.engine.version).toBe('1.13.0');
|
||||||
const area = (id: string) => result.cisControlAreas.find(item => item.id === id);
|
const area = (id: string) => result.cisControlAreas.find(item => item.id === id);
|
||||||
expect(area('cis:run-server:9.1')).toEqual({ id: 'cis:run-server:9.1', scanRunId: 'run-server', sectionId: '9.1', parentSectionId: '9',
|
expect(area('cis:run-server:9.1')).toEqual({ id: 'cis:run-server:9.1', scanRunId: 'run-server', sectionId: '9.1', parentSectionId: '9',
|
||||||
title: 'Domain Profile', findings: 3, fail: 2, pass: 0, other: 1, reported: { pass: 0, fail: 7 } });
|
title: 'Domain Profile', findings: 3, fail: 2, pass: 0, other: 1, reported: { pass: 0, fail: 7 } });
|
||||||
|
|
@ -626,4 +626,67 @@ describe('context GPO verdicts', () => {
|
||||||
expect(conditions.find(item => item.conditionId === 'gpo-lockout-timing-deviates')?.state).toBe('not-satisfied');
|
expect(conditions.find(item => item.conditionId === 'gpo-lockout-timing-deviates')?.state).toBe('not-satisfied');
|
||||||
expect(conditions.find(item => item.conditionId === 'gpo-privileged-rights-deviate')?.state).toBe('satisfied');
|
expect(conditions.find(item => item.conditionId === 'gpo-privileged-rights-deviate')?.state).toBe('satisfied');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('catalogues convenience PIN sign-in and the redirected default OU by their exact identities (catalog 1.11.0)', () => {
|
||||||
|
const pinTarget = { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'SOFTWARE\\Policies\\Microsoft\\Windows\\System', valueName: 'AllowDomainPINLogon' };
|
||||||
|
const siblingTarget = { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'SOFTWARE\\Policies\\Microsoft\\Windows\\System', valueName: 'SyntheticSibling' };
|
||||||
|
const result = buildAssessmentContext({
|
||||||
|
projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-10-13T08:00:00Z', repositoryRevision: 1,
|
||||||
|
observations: [observation('obs-pin', 'id-pin'), observation('obs-sibling', 'id-sibling')],
|
||||||
|
analyses: [{ run: { id: 'a' }, resolutions: [resolution('obs-pin', 'id-pin', pinTarget), resolution('obs-sibling', 'id-sibling', siblingTarget)] }] as never,
|
||||||
|
evaluations: [evaluation('e-pin', 'id-pin', 'fail', true, 'AllowDomainPINLogon'), evaluation('e-sibling', 'id-sibling', 'fail', true, 'SyntheticSibling')],
|
||||||
|
scannerFindings: [{
|
||||||
|
resultProjectionId: 'scanner-finding:occ-ou', sourceKind: 'scanner-finding', sourceResultId: 'occ-ou', sourceTool: 'pingcastle',
|
||||||
|
sourceRuleId: 'S-DefaultOUChanged', outcomeNormalized: 'fail', title: 'Check if default OU location has been changed within the domain.',
|
||||||
|
sourceObservedAt: '2026-10-01T00:00:00Z'
|
||||||
|
}] as unknown as UnifiedResultProjection[],
|
||||||
|
catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG
|
||||||
|
});
|
||||||
|
// Only the exact value name is catalogued; a sibling value under the same key stays an uncatalogued deviation.
|
||||||
|
expect(result.evaluations.map(item => `${item.identityKey}:${item.conditionId}`)).toEqual([
|
||||||
|
'id-pin:gpo-convenience-pin-sign-in-deviates', 'id-sibling:uncatalogued-deviations-gpo']);
|
||||||
|
expect(result.settings.find(item => item.identityKey === 'id-pin')?.clusterIds).toEqual(['physical-local-access-exposure']);
|
||||||
|
expect(result.observations.find(item => item.occurrenceId === 'occ-ou')).toMatchObject({ matchedBy: 'rule-id', conditionId: 'ad-default-container-redirected' });
|
||||||
|
expect(result.findings.find(item => item.occurrenceId === 'occ-ou')?.clusterIds).toEqual(['detection-blind-spots']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('places the catalog 1.12.0 rules of every kind by exact identity, with trusts in recovery', () => {
|
||||||
|
const targets: Array<[string, Record<string, string>]> = [
|
||||||
|
['id-rdp', { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Microsoft\\Windows NT\\Terminal Services', valueName: 'fAllowFullControl' }],
|
||||||
|
['id-anon', { kind: 'security-template', section: 'System Access', key: 'LSAAnonymousNameLookup' }],
|
||||||
|
['id-lmhash', { kind: 'security-template', section: 'Registry Values', key: 'MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\NoLMHash' }],
|
||||||
|
['id-printers', { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Microsoft\\Windows NT\\Printers\\PointAndPrint', valueName: 'RestrictDriverInstallationToAdministrators' }],
|
||||||
|
['id-smartscreen', { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Microsoft\\Windows\\System', valueName: 'EnableSmartScreen' }]
|
||||||
|
];
|
||||||
|
const finding = (tool: string, ruleId: string, occurrence: string, title: string) => ({
|
||||||
|
resultProjectionId: `scanner-finding:${occurrence}`, sourceKind: 'scanner-finding', sourceResultId: occurrence, sourceTool: tool,
|
||||||
|
sourceRuleId: ruleId, outcomeNormalized: 'fail', title, sourceObservedAt: '2026-10-01T00:00:00Z'
|
||||||
|
}) as unknown as UnifiedResultProjection;
|
||||||
|
const result = buildAssessmentContext({
|
||||||
|
projectId: 'dummy-project', assessmentIds: ['dummy-assessment'], generatedAt: '2026-10-13T08:00:00Z', repositoryRevision: 1,
|
||||||
|
observations: targets.map(([identity]) => observation(`obs-${identity}`, identity)),
|
||||||
|
analyses: [{ run: { id: 'a' }, resolutions: targets.map(([identity, target]) => resolution(`obs-${identity}`, identity, target)) }] as never,
|
||||||
|
evaluations: targets.map(([identity], index) => evaluation(`e${index}`, identity, 'fail', true, identity)),
|
||||||
|
scannerFindings: [
|
||||||
|
finding('purple-knight', 'SI000027', 'occ-asrep', 'Users with Kerberos pre-authentication disabled'),
|
||||||
|
finding('purple-knight', 'SI000088', 'occ-trust', 'Outbound forest trust with SID History enabled'),
|
||||||
|
finding('maester', 'EIDSCA.AM03', 'occ-number-matching', 'Authentication Method - Microsoft Authenticator - Require number matching for push notifications.')
|
||||||
|
],
|
||||||
|
catalog: RELEASED_ASSESSMENT_CONTEXT_CATALOG
|
||||||
|
});
|
||||||
|
const setting = (identity: string) => result.settings.find(item => item.identityKey === identity)?.clusterIds;
|
||||||
|
expect(setting('id-rdp')).toEqual(['lateral-movement-exposure']);
|
||||||
|
expect(setting('id-anon')).toEqual(['ad-reconnaissance-exposure']);
|
||||||
|
expect(setting('id-lmhash')).toEqual(['password-guessing-exposure']);
|
||||||
|
// The Microsoft baseline area table places the printer policy tree in credential relay.
|
||||||
|
expect(setting('id-printers')).toEqual(['credential-relay-exposure']);
|
||||||
|
// A value-specific rule under Windows\System does not take over its sibling AllowDomainPINLogon.
|
||||||
|
expect(setting('id-smartscreen')).toEqual(['malicious-code-execution-exposure']);
|
||||||
|
const placed = (occurrence: string) => result.findings.find(item => item.occurrenceId === occurrence)?.clusterIds;
|
||||||
|
expect(placed('occ-asrep')).toEqual(['kerberoasting-exposure']);
|
||||||
|
// User decision 2026-10-12: trusts belong to recovery.
|
||||||
|
expect(placed('occ-trust')).toEqual(['ad-recovery-gaps']);
|
||||||
|
expect(placed('occ-number-matching')).toEqual(['cloud-sign-in-protection-gaps']);
|
||||||
|
expect(result.coverage).toMatchObject({ gpoUncatalogued: 0, scannerUncatalogued: 0 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ describe('vocabulary 1.1.0 for security templates, user rights and cloud finding
|
||||||
// Regression: vocabulary 1.2.0 placed findings in clusters while the stored snapshot kept them unassigned,
|
// Regression: vocabulary 1.2.0 placed findings in clusters while the stored snapshot kept them unassigned,
|
||||||
// because that snapshot is keyed by engine and catalogue version only.
|
// because that snapshot is keyed by engine and catalogue version only.
|
||||||
expect(ASSESSMENT_CONTEXT_ENGINE_VOCABULARY).toBe(CONTEXT_RECOGNITION_VOCABULARY.version);
|
expect(ASSESSMENT_CONTEXT_ENGINE_VOCABULARY).toBe(CONTEXT_RECOGNITION_VOCABULARY.version);
|
||||||
expect(ASSESSMENT_CONTEXT_ENGINE.version).toBe('1.12.0');
|
expect(ASSESSMENT_CONTEXT_ENGINE.version).toBe('1.13.0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps conditional access trusted locations apart from Office trusted locations', () => {
|
it('keeps conditional access trusted locations apart from Office trusted locations', () => {
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@ describe('Finding Reader context layer', () => {
|
||||||
expect(section.textContent).toMatch(/\bfavours?\b/);
|
expect(section.textContent).toMatch(/\bfavours?\b/);
|
||||||
expect(section.textContent).not.toMatch(/exploitable|confirmed attack|begünstigt/i);
|
expect(section.textContent).not.toMatch(/exploitable|confirmed attack|begünstigt/i);
|
||||||
expect(section.textContent).toContain('Credential Relay Exposure: Present');
|
expect(section.textContent).toContain('Credential Relay Exposure: Present');
|
||||||
expect(section.textContent).toContain('3 of 26 conditions met, 2 required');
|
expect(section.textContent).toContain('3 of 31 conditions met, 2 required');
|
||||||
expect(section.textContent).toContain('ATT&CK T1557.001');
|
expect(section.textContent).toContain('ATT&CK T1557.001');
|
||||||
const link = section.querySelector('a')!;
|
const link = section.querySelector('a')!;
|
||||||
expect(link.getAttribute('href')).toMatch(/^https:\/\//);
|
expect(link.getAttribute('href')).toMatch(/^https:\/\//);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
import { act, createElement } from 'react';
|
import { act, createElement, useState } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { renderToStaticMarkup } from 'react-dom/server';
|
import { renderToStaticMarkup } from 'react-dom/server';
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { FloatingResultsWindow } from '../../src/features/linked-results/FloatingResultsWindow';
|
import { FloatingResultsWindow } from '../../src/features/linked-results/FloatingResultsWindow';
|
||||||
import { makeSheet } from '../../src/importers/sheet-factory';
|
import { makeSheet } from '../../src/importers/sheet-factory';
|
||||||
|
import { useFloatingWindowLayer } from '../../src/components/use-floating-window-layer';
|
||||||
|
import { FloatingDataWindow } from '../../src/components/FloatingDataWindow/FloatingDataWindow';
|
||||||
import {
|
import {
|
||||||
resultClusterParkingPosition,
|
resultClusterParkingPosition,
|
||||||
resultClusterCoverHeight,
|
resultClusterCoverHeight,
|
||||||
|
|
@ -22,6 +24,58 @@ function windowState(recordId: string, minimized: boolean, detached = false): Cl
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Result-Fenster-Cluster', () => {
|
describe('Result-Fenster-Cluster', () => {
|
||||||
|
it.each(['light', 'dark'])('keeps restored Results above every minimized sibling after pointer and keyboard activation (%s)', theme => {
|
||||||
|
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
document.documentElement.dataset.theme = theme;
|
||||||
|
vi.stubGlobal('ResizeObserver', vi.fn(() => ({ observe: vi.fn(), disconnect: vi.fn() })));
|
||||||
|
const host = document.createElement('div'); document.body.append(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
function Cluster() {
|
||||||
|
const [windows, setWindows] = useState([windowState('first', true), windowState('second', true), windowState('third', false)]);
|
||||||
|
const { layer, activate } = useFloatingWindowLayer();
|
||||||
|
return createElement('div', null,
|
||||||
|
...windows.map(item => createElement(FloatingResultsWindow, {
|
||||||
|
key: item.recordId, id: item.recordId, title: `Results (1) – ${item.recordId}`, sheets: [],
|
||||||
|
sections: [{ title: 'Test result', value: `Original evidence ${item.recordId}` }],
|
||||||
|
color: 'var(--status-high)', zIndex: 2100, offset: 0, search: '', minimized: item.minimized,
|
||||||
|
clusterPosition: { left: 40, top: 132 }, clusterIndex: item.minimized ? windows.filter(w => w.minimized).indexOf(item) : 0,
|
||||||
|
clusterCount: windows.length, clusterLayer: layer, onActivateCluster: activate,
|
||||||
|
onActivate: () => undefined, onClose: () => undefined,
|
||||||
|
onToggleMinimize: () => setWindows(current => toggleResultWindowMinimized(current, item.recordId))
|
||||||
|
})), createElement(FloatingDataWindow, { title: 'Other window', onClose: () => undefined, children: 'Other content' }));
|
||||||
|
}
|
||||||
|
const dialog = (id: string) => host.querySelector<HTMLElement>(`#${id}`) ?? host.querySelector<HTMLElement>(`.results-window[aria-label="Results (1) – ${id}"]`)!;
|
||||||
|
const z = (element: HTMLElement) => Number(element.style.zIndex);
|
||||||
|
const assertForeground = (id: string) => {
|
||||||
|
const active = dialog(id);
|
||||||
|
expect(active.classList.contains('is-minimized')).toBe(false);
|
||||||
|
expect(active.querySelector('.results-window-body')?.textContent).toContain(`Original evidence ${id}`);
|
||||||
|
for (const sibling of host.querySelectorAll<HTMLElement>('.results-window.is-minimized')) expect(z(active)).toBeGreaterThan(z(sibling));
|
||||||
|
expect(active.querySelector('.results-window-head')?.lastElementChild?.lastElementChild?.getAttribute('aria-label')).toBe('Close results window');
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
act(() => root.render(createElement(Cluster)));
|
||||||
|
assertForeground('third');
|
||||||
|
for (const id of ['first', 'second', 'third', 'first']) {
|
||||||
|
act(() => dialog(id).dispatchEvent(new Event('pointerdown', { bubbles: true })));
|
||||||
|
act(() => dialog(id).querySelector<HTMLElement>('.results-window-toggle')!.click());
|
||||||
|
assertForeground(id);
|
||||||
|
const sibling = host.querySelector<HTMLElement>('.results-window.is-minimized')!;
|
||||||
|
act(() => sibling.querySelector<HTMLElement>('.results-window-toggle')!.focus());
|
||||||
|
assertForeground(id);
|
||||||
|
const other = document.querySelector<HTMLElement>('.floating-data-window')!;
|
||||||
|
act(() => other.dispatchEvent(new Event('pointerdown', { bubbles: true })));
|
||||||
|
expect(z(other)).toBeGreaterThan(z(dialog(id)));
|
||||||
|
act(() => dialog(id).dispatchEvent(new Event('pointerdown', { bubbles: true })));
|
||||||
|
expect(z(dialog(id))).toBeGreaterThan(z(other));
|
||||||
|
}
|
||||||
|
act(() => dialog('first').querySelector('.results-window-head')!.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })));
|
||||||
|
expect(dialog('first').classList.contains('is-maximized')).toBe(true);
|
||||||
|
assertForeground('first');
|
||||||
|
} finally {
|
||||||
|
act(() => root.unmount()); host.remove(); vi.unstubAllGlobals(); delete document.documentElement.dataset.theme;
|
||||||
|
}
|
||||||
|
});
|
||||||
it('parkt minimierte Results oben rechts wie in der Referenzansicht', () => {
|
it('parkt minimierte Results oben rechts wie in der Referenzansicht', () => {
|
||||||
expect(resultClusterParkingPosition(1848, 760, 640, 142)).toEqual({ left: 1008, top: 188 });
|
expect(resultClusterParkingPosition(1848, 760, 640, 142)).toEqual({ left: 1008, top: 188 });
|
||||||
expect(resultClusterParkingPosition(440, 428, 42, 142)).toEqual({ left: 0, top: 42 });
|
expect(resultClusterParkingPosition(440, 428, 42, 142)).toEqual({ left: 0, top: 42 });
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,31 @@ describe('scanner first read', () => {
|
||||||
expect(item.fields.find(field => field.label === 'Result')?.value).toBe(result);
|
expect(item.fields.find(field => field.label === 'Result')?.value).toBe(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Regression 2026-10-13: a list following the result statement (one item per line, or flattened behind the
|
||||||
|
// introducing colon) appeared completely in the Problem block; it belongs to Results like tables and headings.
|
||||||
|
it.each([
|
||||||
|
['flattened export (Result next to Status)', [['Status', 'Failed'], ['Result',
|
||||||
|
'Synthetic emergency access * user: Synthetic A (00000000-0000-0000-0000-000000000001) These synthetic policies lack the exclusion: '
|
||||||
|
+ '- [Synthetic policy - one](https://entra.microsoft.com/#synthetic-1) - [Synthetic policy two](https://entra.microsoft.com/#synthetic-2)']]],
|
||||||
|
['native ResultDetail with line breaks', [['Result', 'Failed'], ['ResultDetail',
|
||||||
|
'## Test result\n\nSynthetic emergency access * user: Synthetic A (00000000-0000-0000-0000-000000000001) These synthetic policies lack the exclusion:\n'
|
||||||
|
+ ' - [Synthetic policy - one](https://entra.microsoft.com/#synthetic-1)\n - [Synthetic policy two](https://entra.microsoft.com/#synthetic-2)']]]
|
||||||
|
] as Array<[string, Array<[string, string]>]>)('Maester %s: the Problem ends before the listed items', (_variant, fields) => {
|
||||||
|
const item = record([['ID', 'MT.9002'], ['Title', 'Synthetic break glass finding'], ['Description', 'Synthetic description.'], ...fields]);
|
||||||
|
const { container, values } = render(item, 'maester');
|
||||||
|
expect(values('.finding-first-read')).toEqual({ Result:
|
||||||
|
'Synthetic emergency access * user: Synthetic A (00000000-0000-0000-0000-000000000001) These synthetic policies lack the exclusion:' });
|
||||||
|
const problem = container.querySelector('.finding-first-read')?.textContent ?? '';
|
||||||
|
expect(problem).not.toContain('Synthetic policy');
|
||||||
|
expect(container.querySelector('.finding-first-read li, .finding-first-read a')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Maester: a test result that opens with a list states no problem paragraph from later text', () => {
|
||||||
|
const item = record([['ID', 'MT.9003'], ['Title', 'Synthetic finding'], ['Result', 'Failed'],
|
||||||
|
['ResultDetail', '- [Synthetic one](https://entra.microsoft.com/#1)\n- [Synthetic two](https://entra.microsoft.com/#2)\n\nSynthetic trailing note.']]);
|
||||||
|
expect(render(item, 'maester').values('.finding-first-read').Result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('Maester export without a Status column keeps Result as the status, not as a problem statement', () => {
|
it('Maester export without a Status column keeps Result as the status, not as a problem statement', () => {
|
||||||
const item = record([['ID', 'MT.9002'], ['Title', 'Synthetic finding'], ['Result', 'Failed'], ['Description', 'Synthetic description.']]);
|
const item = record([['ID', 'MT.9002'], ['Title', 'Synthetic finding'], ['Result', 'Failed'], ['Description', 'Synthetic description.']]);
|
||||||
expect(render(item, 'maester').container.querySelector('.finding-first-read.policy-problem')).toBeNull();
|
expect(render(item, 'maester').container.querySelector('.finding-first-read.policy-problem')).toBeNull();
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,27 @@ function setup(theme: string) {
|
||||||
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.replaceChildren(); localStorage.clear(); resetEnginePreferencesForTests(); resetAssessmentContextRequestsForTests(); });
|
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.replaceChildren(); localStorage.clear(); resetEnginePreferencesForTests(); resetAssessmentContextRequestsForTests(); });
|
||||||
|
|
||||||
describe.each(['light', 'dark'])('Complete Security HTML export (%s)', theme => {
|
describe.each(['light', 'dark'])('Complete Security HTML export (%s)', theme => {
|
||||||
|
it('resolves the original assessment and German date from a scan-only selection', async () => {
|
||||||
|
const fixture = setup(theme);
|
||||||
|
fixture.app.activeAssessmentIds = [];
|
||||||
|
fixture.app.activeScanRunIds = ['synthetic-scan'];
|
||||||
|
vi.mocked(AssessorApiClient.prototype.listProjectAssessmentHistory).mockResolvedValue({
|
||||||
|
assessments: [{ id: 'synthetic-assessment', name: 'Assessment 2026-07-28', effectiveAt: '2026-07-28' }],
|
||||||
|
scanRuns: [{ id: 'synthetic-scan', assessmentId: 'synthetic-assessment' }]
|
||||||
|
} as never);
|
||||||
|
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
|
||||||
|
try {
|
||||||
|
await act(async () => root.render(<DashboardPanel onOpenWork={vi.fn()}/>));
|
||||||
|
await act(async () => [...host.querySelectorAll<HTMLButtonElement>('[role="tab"]')].find(tab => tab.textContent === 'Security')!.click());
|
||||||
|
const button = [...host.querySelectorAll<HTMLButtonElement>('button')].find(item => item.textContent === 'HTML Security Export')!;
|
||||||
|
await act(async () => { button.click(); await vi.dynamicImportSettled(); });
|
||||||
|
expect(fixture.download).toHaveBeenCalledTimes(1);
|
||||||
|
const report = new DOMParser().parseFromString(fixture.download.mock.calls[0][0].text, 'text/html');
|
||||||
|
expect(report.querySelector('.security-export-assessment')?.textContent).toBe('Assessment · 28.07.2026');
|
||||||
|
expect(report.querySelector('.security-export-summary')?.textContent).toContain('Die 2 Abweichungen');
|
||||||
|
expect(fixture.app.activeAssessmentIds).toEqual([]); expect(fixture.app.activeScanRunIds).toEqual(['synthetic-scan']);
|
||||||
|
} finally { await act(async () => root.unmount()); host.remove(); }
|
||||||
|
});
|
||||||
it('exports the complete actual tab including closed details, all native rows, branding, confidentiality and exact scope without changing Work', async () => {
|
it('exports the complete actual tab including closed details, all native rows, branding, confidentiality and exact scope without changing Work', async () => {
|
||||||
const fixture = setup(theme); const before = JSON.stringify(fixture); const openWork = vi.fn();
|
const fixture = setup(theme); const before = JSON.stringify(fixture); const openWork = vi.fn();
|
||||||
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
|
const host = document.createElement('div'); document.body.append(host); const root = createRoot(host);
|
||||||
|
|
@ -90,9 +111,26 @@ describe.each(['light', 'dark'])('Complete Security HTML export (%s)', theme =>
|
||||||
const report = new DOMParser().parseFromString(output.text, 'text/html');
|
const report = new DOMParser().parseFromString(output.text, 'text/html');
|
||||||
expect(report.documentElement.dataset.theme).toBe(theme); expect(report.documentElement.dataset.density).toBe('reading');
|
expect(report.documentElement.dataset.theme).toBe(theme); expect(report.documentElement.dataset.density).toBe('reading');
|
||||||
expect(report.querySelector('title')?.textContent).toContain('Synthetic <client>');
|
expect(report.querySelector('title')?.textContent).toContain('Synthetic <client>');
|
||||||
|
expect(report.querySelector('h1')?.textContent).toBe('Security Report');
|
||||||
|
expect(report.querySelector('.security-export-client')?.textContent).toBe('Synthetic <client>');
|
||||||
|
expect(report.querySelector('.security-export-assessment')?.textContent).toBe('Assessment: Synthetic timeslot · 12.10.2026');
|
||||||
|
expect(report.body.textContent).not.toContain('Vollständige Security-Ansicht');
|
||||||
|
const environment = report.querySelector('.security-export-environment')!;
|
||||||
|
expect(environment.querySelector('h2')?.textContent).toBe('System- und Umgebungsübersicht');
|
||||||
|
expect(environment.querySelectorAll('.client-data-fact')).toHaveLength(6);
|
||||||
|
expect(report.querySelector('.client-security-panel .client-security-hardfacts')).toBeNull();
|
||||||
|
expect(environment.compareDocumentPosition(report.querySelector('.client-security-header')!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||||
|
expect(report.querySelector('.security-export-summary')?.textContent).toContain('Die 2 Abweichungen verteilen sich auf 2 Security Domains und 1 Problemfeld.');
|
||||||
|
expect(report.querySelector('.security-export-summary')?.textContent).toContain('Cloud Privileged Access Exposure (2)');
|
||||||
|
const themeRadios = [...report.querySelectorAll<HTMLInputElement>('.security-export-theme input')];
|
||||||
|
expect(themeRadios.map(input => input.value)).toEqual(['dark', 'system', 'light']);
|
||||||
|
expect(themeRadios.find(input => input.checked)?.value).toBe(theme);
|
||||||
|
themeRadios[1].click(); expect(themeRadios[1].checked).toBe(true);
|
||||||
|
themeRadios[2].click(); expect(themeRadios[2].checked).toBe(true); expect(themeRadios[1].checked).toBe(false);
|
||||||
|
themeRadios[0].click(); expect(themeRadios[0].checked).toBe(true); expect(themeRadios[2].checked).toBe(false);
|
||||||
expect(report.querySelector('.security-export-confidential')?.textContent).toBe('VERTRAULICH');
|
expect(report.querySelector('.security-export-confidential')?.textContent).toBe('VERTRAULICH');
|
||||||
expect(report.querySelector('img[alt="ASSESSOR Logo"]')?.getAttribute('src')).toMatch(/^data:image/);
|
expect(report.querySelector('img[alt="ASSESSOR Logo"]')?.getAttribute('src')).toMatch(/^data:image/);
|
||||||
expect(report.querySelector('style')?.textContent).toContain('width:min(2320px,calc(100vw * 2 / 3))');
|
expect(report.querySelector('style')?.textContent).toContain('width:min(2320px,calc(100vw / 2))');
|
||||||
expect(/minmax\(0,\s*2fr\)\s+minmax\(0,\s*1fr\)/.test(report.querySelector('style')?.textContent ?? '')).toBe(true);
|
expect(/minmax\(0,\s*2fr\)\s+minmax\(0,\s*1fr\)/.test(report.querySelector('style')?.textContent ?? '')).toBe(true);
|
||||||
expect(report.querySelector('.client-security-remediation strong')?.textContent).toBe('2');
|
expect(report.querySelector('.client-security-remediation strong')?.textContent).toBe('2');
|
||||||
expect(report.querySelectorAll('.client-security-group')).toHaveLength(panel.querySelectorAll('.client-security-group').length);
|
expect(report.querySelectorAll('.client-security-group')).toHaveLength(panel.querySelectorAll('.client-security-group').length);
|
||||||
|
|
@ -111,7 +149,8 @@ describe.each(['light', 'dark'])('Complete Security HTML export (%s)', theme =>
|
||||||
expect(popup.querySelector('header')?.lastElementChild?.getAttribute('popovertargetaction')).toBe('hide');
|
expect(popup.querySelector('header')?.lastElementChild?.getAttribute('popovertargetaction')).toBe('hide');
|
||||||
const reportCss = report.querySelector('style')?.textContent ?? '';
|
const reportCss = report.querySelector('style')?.textContent ?? '';
|
||||||
expect(reportCss).not.toContain('zoom:');
|
expect(reportCss).not.toContain('zoom:');
|
||||||
expect(reportCss).toContain('.security-export-popup-body{overflow:auto;overscroll-behavior:contain;min-height:0;padding:var(--panel-inset);font-size:var(--type-table-body-size)}');
|
expect(reportCss).toContain(':root{--type-table-body-size:var(--type-label-size);--type-body-size:14px;--type-h3-size:var(--type-h4-size)}');
|
||||||
|
expect(reportCss).toContain('.security-export-popup-body{overflow:auto;overscroll-behavior:contain;min-height:0;padding:var(--panel-inset);font-size:var(--type-body-size)}');
|
||||||
expect(reportCss).toContain('.client-security-map-scroll>svg{width:1280px;max-width:none}');
|
expect(reportCss).toContain('.client-security-map-scroll>svg{width:1280px;max-width:none}');
|
||||||
expect(reportCss).toContain('@container security-report (max-width:1800px)');
|
expect(reportCss).toContain('@container security-report (max-width:1800px)');
|
||||||
expect(reportCss).toContain('.security-export-popup .security-export-popup-body p');
|
expect(reportCss).toContain('.security-export-popup .security-export-popup-body p');
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,27 @@ const snapshot = Object.freeze({ markup: '<div class="client-security-panel"><de
|
||||||
versions: [['Datenstand', 'Revision 1']] as const });
|
versions: [['Datenstand', 'Revision 1']] as const });
|
||||||
const port: SecurityViewReadPort = { read: () => snapshot };
|
const port: SecurityViewReadPort = { read: () => snapshot };
|
||||||
describe('Standalone Security view renderer', () => {
|
describe('Standalone Security view renderer', () => {
|
||||||
|
it('formats separate assessments and an exact reconciled German overview without altering published counts', () => {
|
||||||
|
const distribution = { total: 10, domains: [{ label: 'System <demo>', count: 7 }, { label: 'Identity', count: 3 }, { label: 'Empty', count: 0 }],
|
||||||
|
categories: [{ label: 'Category A', count: 4 }, { label: 'Category B', count: 6 }, { label: 'Empty', count: 0 }] };
|
||||||
|
const before = JSON.stringify(distribution);
|
||||||
|
const html = buildSecurityViewReportHtml({ read: () => ({ ...snapshot, distribution,
|
||||||
|
assessmentDetails: [{ name: 'Assessment 2026-07-28', effectiveAt: '2026-07-28' }, { name: 'Synthetic comparison', effectiveAt: '2026-10-12T23:00:00Z' }],
|
||||||
|
environmentMarkup: '<section class="client-security-hardfacts">Original environment</section>' }) });
|
||||||
|
const dom = new JSDOM(html.replace(/<style>[\s\S]*?<\/style>/g, ''));
|
||||||
|
const doc = dom.window.document;
|
||||||
|
expect(doc.querySelector('h1')?.textContent).toBe('Security Report');
|
||||||
|
expect([...doc.querySelectorAll('.security-export-assessment')].map(item => item.textContent)).toEqual(['Assessment · 28.07.2026', 'Assessment: Synthetic comparison · 13.10.2026']);
|
||||||
|
expect(doc.querySelector('.security-export-summary')?.textContent).toBe('Die 10 Abweichungen verteilen sich auf 2 Security Domains und 2 Problemfelder. Den größten Anteil trägt System <demo> (7, 70 %); das am stärksten belegte Problemfeld ist Category B (6).');
|
||||||
|
expect(html).not.toContain('Vollständige Security-Ansicht');
|
||||||
|
expect(html.indexOf('Original environment')).toBeLessThan(html.indexOf(snapshot.markup));
|
||||||
|
expect(JSON.stringify(distribution)).toBe(before);
|
||||||
|
dom.window.close();
|
||||||
|
expect(() => buildSecurityViewReportHtml({ read: () => ({ ...snapshot, distribution: { ...distribution, total: 11 } }) })).toThrow('reconcile');
|
||||||
|
expect(buildSecurityViewReportHtml({ read: () => ({ ...snapshot, distribution: { total: 0, domains: [], categories: [] } }) })).toContain('keine Abweichungen');
|
||||||
|
expect(buildSecurityViewReportHtml({ read: () => ({ ...snapshot, assessmentDetails: [{ name: 'Synthetic invalid date', effectiveAt: '2026-02-31' }] }) })).toContain('Datum nicht verfügbar');
|
||||||
|
expect(() => buildSecurityViewReportHtml({ read: () => ({ ...snapshot, environmentMarkup: '<img onerror="bad()">' }) })).toThrow('active content');
|
||||||
|
});
|
||||||
it.each(['light', 'dark'] as const)('retains native script-free category navigation and independent maximizable popups (%s)', theme => {
|
it.each(['light', 'dark'] as const)('retains native script-free category navigation and independent maximizable popups (%s)', theme => {
|
||||||
const markup = '<a href="#security-category-synthetic">Show synthetic category</a><div style="height:1400px">Synthetic overview</div><details class="client-security-group" id="security-category-synthetic"><summary>Synthetic category</summary><div class="client-security-group-content">Original synthetic finding</div></details><button popovertarget="synthetic-popup">Results</button><div style="height:1400px">Synthetic footer</div>';
|
const markup = '<a href="#security-category-synthetic">Show synthetic category</a><div style="height:1400px">Synthetic overview</div><details class="client-security-group" id="security-category-synthetic"><summary>Synthetic category</summary><div class="client-security-group-content">Original synthetic finding</div></details><button popovertarget="synthetic-popup">Results</button><div style="height:1400px">Synthetic footer</div>';
|
||||||
const popups = '<section popover="auto" id="synthetic-popup" class="security-export-popup" role="dialog" aria-label="Synthetic results"><header><h2>Synthetic results</h2><button class="btn" popovertarget="synthetic-popup" popovertargetaction="hide" aria-label="Close synthetic results">×</button></header><div class="security-export-popup-body"><dl>' + Array.from({ length: 9 }, (_, i) => `<div><dt>Fact ${i}</dt><dd>Original ${i}</dd></div>`).join('') + '</dl><div style="height:1600px">Synthetic long evidence</div><p>Evidence end</p></div></section>';
|
const popups = '<section popover="auto" id="synthetic-popup" class="security-export-popup" role="dialog" aria-label="Synthetic results"><header><h2>Synthetic results</h2><button class="btn" popovertarget="synthetic-popup" popovertargetaction="hide" aria-label="Close synthetic results">×</button></header><div class="security-export-popup-body"><dl>' + Array.from({ length: 9 }, (_, i) => `<div><dt>Fact ${i}</dt><dd>Original ${i}</dd></div>`).join('') + '</dl><div style="height:1600px">Synthetic long evidence</div><p>Evidence end</p></div></section>';
|
||||||
|
|
@ -26,7 +47,8 @@ describe('Standalone Security view renderer', () => {
|
||||||
expect(html).toContain('href="#security-category-content-synthetic"');
|
expect(html).toContain('href="#security-category-content-synthetic"');
|
||||||
expect(html).toContain('<div class="client-security-group-content" id="security-category-content-synthetic">');
|
expect(html).toContain('<div class="client-security-group-content" id="security-category-content-synthetic">');
|
||||||
expect(html).toContain('overscroll-behavior:contain');
|
expect(html).toContain('overscroll-behavior:contain');
|
||||||
expect(html).toContain('100vw * 2 / 3');
|
expect(html).toContain('100vw / 2');
|
||||||
|
expect(html).toContain('max-width:100%;margin:0 auto');
|
||||||
expect(html).not.toContain('zoom:');
|
expect(html).not.toContain('zoom:');
|
||||||
expect(html).toContain('font-size:var(--type-table-body-size)');
|
expect(html).toContain('font-size:var(--type-table-body-size)');
|
||||||
expect(html).toContain('@container security-report (max-width:1800px)');
|
expect(html).toContain('@container security-report (max-width:1800px)');
|
||||||
|
|
@ -55,7 +77,7 @@ describe('Standalone Security view renderer', () => {
|
||||||
expect(html.includes(readFileSync(`src/styles/${file}.css`, 'utf8'))).toBe(true);
|
expect(html.includes(readFileSync(`src/styles/${file}.css`, 'utf8'))).toBe(true);
|
||||||
}
|
}
|
||||||
expect(html).toContain(snapshot.markup); expect(html).toContain('Synthetic <client>');
|
expect(html).toContain(snapshot.markup); expect(html).toContain('Synthetic <client>');
|
||||||
expect(html).toContain('width:min(1910px,calc(100vw * 2 / 3))'); expect(html).toContain('VERTRAULICH');
|
expect(html).toContain('width:min(1910px,calc(100vw / 2))'); expect(html).toContain('VERTRAULICH');
|
||||||
expect(html).toContain('data:font/woff2;base64,'); expect(html).toContain('data:image/png;base64,');
|
expect(html).toContain('data:font/woff2;base64,'); expect(html).toContain('data:image/png;base64,');
|
||||||
expect(html).not.toMatch(/<script|<(?:img|link)\b[^>]*(?:src|href)=["']https?:/i);
|
expect(html).not.toMatch(/<script|<(?:img|link)\b[^>]*(?:src|href)=["']https?:/i);
|
||||||
expect(JSON.stringify(snapshot)).toBe(before);
|
expect(JSON.stringify(snapshot)).toBe(before);
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ describe('local ASSESSOR HTTP server', () => {
|
||||||
|
|
||||||
const health = await fetch(`${baseUrl}/api/v1/health`);
|
const health = await fetch(`${baseUrl}/api/v1/health`);
|
||||||
expect(await health.json()).toEqual({
|
expect(await health.json()).toEqual({
|
||||||
status: 'ok', application: 'ASSESSOR', productVersion: '2.25.2', apiVersion: 1, repositoryRevision: 0, persistence: 'memory'
|
status: 'ok', application: 'ASSESSOR', productVersion: '2.25.6', apiVersion: 1, repositoryRevision: 0, persistence: 'memory'
|
||||||
});
|
});
|
||||||
expect(health.headers.get('access-control-allow-origin')).toBeNull();
|
expect(health.headers.get('access-control-allow-origin')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
@ -200,7 +200,7 @@ describe('local ASSESSOR HTTP server', () => {
|
||||||
expect(assessmentContext).toMatchObject({
|
expect(assessmentContext).toMatchObject({
|
||||||
version: 1, engine: { id: 'assessment-context-engine' }, projectId: project.id, assessmentIds: [assessment.id],
|
version: 1, engine: { id: 'assessment-context-engine' }, projectId: project.id, assessmentIds: [assessment.id],
|
||||||
facts: [], coverage: { observations: 0 },
|
facts: [], coverage: { observations: 0 },
|
||||||
clusters: expect.arrayContaining([expect.objectContaining({ clusterId: 'credential-relay-exposure', state: 'undetermined', unknown: 26 })])
|
clusters: expect.arrayContaining([expect.objectContaining({ clusterId: 'credential-relay-exposure', state: 'undetermined', unknown: 31 })])
|
||||||
});
|
});
|
||||||
const cachedContext = await (await fetch(`${baseUrl}/api/v1/projects/${project.id}/assessment-context?assessmentIds=${assessment.id}`)).json();
|
const cachedContext = await (await fetch(`${baseUrl}/api/v1/projects/${project.id}/assessment-context?assessmentIds=${assessment.id}`)).json();
|
||||||
expect(cachedContext.generatedAt).toBe(assessmentContext.generatedAt);
|
expect(cachedContext.generatedAt).toBe(assessmentContext.generatedAt);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue