From e12b269a66c6f332470de7c68d735927e3549a5c Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Sun, 1 Nov 2026 08:54:17 +0100 Subject: [PATCH 01/55] Create Shortest Paths from Owned Principals to Disabled Objects.yml Origin https://github.com/SpecterOps/BloodHoundQueryLibrary/pull/56 --- ...rom Owned Principals to Disabled Objects.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 queries/Shortest Paths from Owned Principals to Disabled Objects.yml diff --git a/queries/Shortest Paths from Owned Principals to Disabled Objects.yml b/queries/Shortest Paths from Owned Principals to Disabled Objects.yml new file mode 100644 index 0000000..ad7efa5 --- /dev/null +++ b/queries/Shortest Paths from Owned Principals to Disabled Objects.yml @@ -0,0 +1,17 @@ +name: Shortest Paths from Owned Principals to Disabled Objects +guid: 22dd14ca-2893-45ae-b07a-1b9868e93772 +prebuilt: false +platforms: Active Directory +category: Shortest Paths +description: WARNING! MANY-TO-MANY SHORTEST PATH QUERIES USE EXCESSIVE SYSTEM RESOURCES AND TYPICALLY WILL NOT COMPLETE +query: |- + // MANY TO MANY SHORTEST PATH QUERIES USE EXCESSIVE SYSTEM RESOURCES AND TYPICALLY WILL NOT COMPLETE + MATCH p=shortestPath((source:Tag_Owned)-[:ALL_ATTACK_PATHS*1..]->(target:Base)) + WHERE source<>target + AND (target:User OR target:Computer) + AND target.enabled = false + RETURN p + LIMIT 1000 +revision: 1 +resources: +acknowledgements: NCV, @nickvourd From 33bbbce91d378264e7641d226f279b92aed0a9c4 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Sun, 1 Nov 2026 09:00:19 +0100 Subject: [PATCH 02/55] Create Non-Tier Zero Principals with Control of Disabled Objects.yml Origin https://github.com/SpecterOps/BloodHoundQueryLibrary/pull/56 --- ...ncipals with Control of Disabled Objects.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 queries/Non-Tier Zero Principals with Control of Disabled Objects.yml diff --git a/queries/Non-Tier Zero Principals with Control of Disabled Objects.yml b/queries/Non-Tier Zero Principals with Control of Disabled Objects.yml new file mode 100644 index 0000000..f2bf462 --- /dev/null +++ b/queries/Non-Tier Zero Principals with Control of Disabled Objects.yml @@ -0,0 +1,17 @@ +name: Non-Tier Zero Principals with Control of Disabled Objects +guid: dbe1220a-2f30-4af1-8090-a79f347cb5ba +prebuilt: false +platforms: Active Directory +category: Active Directory Hygiene +description: Identifies non-Tier Zero users, groups, or computers with dangerous control permissions over disabled user or computer objects. +query: |- + MATCH p = (source)-[r:GenericAll|GenericWrite|WriteDacl|WriteAccountRestrictions|WriteOwner|Owns|OwnsLimitedRights|WriteOwnerLimitedRights]->(target) + WHERE (source:User OR source:Group OR source:Computer) + AND NOT (source:Tag_Tier_Zero) + AND (target:User OR target:Computer) + AND target.enabled = false + RETURN p + LIMIT 1000 +revision: 1 +resources: +acknowledgements: NCV, @nickvourd From bb77fc653b8066a36b10f78445b527797330db09 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Sun, 1 Nov 2026 09:02:44 +0100 Subject: [PATCH 03/55] Create All Domain Controllers.yml Origin https://github.com/SpecterOps/BloodHoundQueryLibrary/pull/56 --- queries/All Domain Controllers.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 queries/All Domain Controllers.yml diff --git a/queries/All Domain Controllers.yml b/queries/All Domain Controllers.yml new file mode 100644 index 0000000..c5da2ef --- /dev/null +++ b/queries/All Domain Controllers.yml @@ -0,0 +1,17 @@ +name: All Domain Controllers +guid: b6c75830-f4d1-4c9d-b1ad-105687ee6990 +prebuilt: false +platforms: Active Directory +category: Domain Information +description: Displays all Domain Controllers (Writeable and Read-only) connected to their domain-specific Domain Controllers group. +query: |- + MATCH p = (g:Group)<-[:MemberOf*1..]-(:Computer) + WHERE g.objectid ENDS WITH '-516' // (Writeable) Domain Controllers + OR g.objectid ENDS WITH '-521' // Read-Only Domain Controllers + RETURN p + LIMIT 1000 +revision: 1 +resources: +- https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-groups#domain-controllers +- https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-groups#read-only-domain-controllers +acknowledgements: NCV, @nickvourd From b00b934b8535c0eba22009e1e1d9d6e33f55b100 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Sun, 1 Nov 2026 09:24:13 +0100 Subject: [PATCH 04/55] "AdminSDHolder to protected objects relationship" replaces "AdminSDHolder protected Accounts and Groups" Build-in "AdminSDHolder to protected objects relationship" query relies on BloodHound's native AdminSDHolder implementation, use it in favor of the legacy "AdminSDHolder protected Accounts and Groups". --- ...SDHolder protected Accounts and Groups.yml | 20 ------------------- ...lder to protected objects relationship.yml | 6 ++++-- 2 files changed, 4 insertions(+), 22 deletions(-) delete mode 100644 queries/AdminSDHolder protected Accounts and Groups.yml diff --git a/queries/AdminSDHolder protected Accounts and Groups.yml b/queries/AdminSDHolder protected Accounts and Groups.yml deleted file mode 100644 index 35bc2b4..0000000 --- a/queries/AdminSDHolder protected Accounts and Groups.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: AdminSDHolder protected Accounts and Groups -guid: 5ee2f40e-a55c-4140-ab8a-91746ba3752b -prebuilt: false -platforms: Active Directory -category: Domain Information -description: Objects whose permissions are set by SDProp to the template AdminSDHolder object as per MS-ADTS 3.1.1.6.1.2 Protected Objects. Does not exclude objects if specified in dSHeuristics dwAdminSDExMask -query: |- - MATCH (n:Base)-[:MemberOf*0..]->(m:Group) - WHERE ( - n.objectid =~ ".*-(S-1-5-32-544|S-1-5-32-548|S-1-5-32-549|S-1-5-32-550|S-1-5-32-551|S-1-5-32-552|518|512|519)$" // Groups - OR m.objectid =~ ".*-(S-1-5-32-544|S-1-5-32-548|S-1-5-32-549|S-1-5-32-550|S-1-5-32-551|S-1-5-32-552|518|512|519)$" // Members of groups - OR n.objectid =~ ".*-(500|502|516|521)$" // Direct objects - ) - RETURN n -revision: 1 -resources: -- https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/a0d0b4fa-2895-4c64-b182-ba64ad0f84b8 -- https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/plan/security-best-practices/appendix-c--protected-accounts-and-groups-in-active-directory -acknowledgements: Martin Sohn Christensen, @martinsohndk - diff --git a/queries/AdminSDHolder to protected objects relationship.yml b/queries/AdminSDHolder to protected objects relationship.yml index df1d9cd..0986948 100644 --- a/queries/AdminSDHolder to protected objects relationship.yml +++ b/queries/AdminSDHolder to protected objects relationship.yml @@ -3,11 +3,13 @@ guid: c751f95c-8bb0-4be4-b027-84f5709c91d2 prebuilt: true platforms: Active Directory category: Active Directory Hygiene -description: +description: Objects whose permissions are set by SDProp to the template AdminSDHolder object as per MS-ADTS 3.1.1.6.1.2 Protected Objects. query: |- MATCH p=(n)-[:ProtectAdminGroups]->(m) RETURN p LIMIT 1000 -revision: 1 +revision: 2 resources: +- https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/a0d0b4fa-2895-4c64-b182-ba64ad0f84b8 +- https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/plan/security-best-practices/appendix-c--protected-accounts-and-groups-in-active-directory acknowledgements: From cff419d70eb2336817aa9cb8a4f22ca3e7ccbac1 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Sun, 1 Nov 2026 09:53:25 +0100 Subject: [PATCH 05/55] Change "Non-Tier Zero principals with control of AdminSDHolder" to use all traversable AD edges. --- ...Non-Tier Zero principals with control of AdminSDHolder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/queries/Non-Tier Zero principals with control of AdminSDHolder.yml b/queries/Non-Tier Zero principals with control of AdminSDHolder.yml index 51c3eec..f5a39ac 100644 --- a/queries/Non-Tier Zero principals with control of AdminSDHolder.yml +++ b/queries/Non-Tier Zero principals with control of AdminSDHolder.yml @@ -5,11 +5,11 @@ platforms: Active Directory category: Dangerous Privileges description: query: |- - MATCH p=(n:Group)-[r:Owns|GenericAll|GenericWrite|WriteOwner|WriteDacl|ForceChangePassword|AllExtendedRights|AddMember|AllowedToDelegate|CoerceToTGT|AllowedToAct|AdminTo|CanPSRemote|CanRDP|ExecuteDCOM|HasSIDHistory|AddSelf|DCSync|ReadLAPSPassword|ReadGMSAPassword|DumpSMSAPassword|SQLAdmin|AddAllowedToAct|WriteSPN|AddKeyCredentialLink|SyncLAPSPassword|WriteAccountRestrictions|WriteOwnerLimitedRights|OwnsLimitedRights]->(m:Container) + MATCH p=(n:Group)-[r:AD_ATTACK_PATHS]->(m:Container) WHERE NOT (n:Tag_Tier_Zero) AND m.name STARTS WITH "ADMINSDHOLDER@" RETURN p -revision: 2 +revision: 3 resources: https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/plan/security-best-practices/appendix-c--protected-accounts-and-groups-in-active-directory#adminsdholder acknowledgements: Martin Sohn Christensen, @martinsohndk From 64c0f465099bc95c751e8fa6d094f1359c943f6c Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Sun, 1 Nov 2026 10:00:29 +0100 Subject: [PATCH 06/55] Update Computer owners who can obtain LAPS passwords.yml Use BloodHound's native Owns edges. Exclude disabled computers as LAPS attacks on those are not possible. --- .../Computer owners who can obtain LAPS passwords.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/queries/Computer owners who can obtain LAPS passwords.yml b/queries/Computer owners who can obtain LAPS passwords.yml index 8fd33ee..d3bc8f0 100644 --- a/queries/Computer owners who can obtain LAPS passwords.yml +++ b/queries/Computer owners who can obtain LAPS passwords.yml @@ -3,12 +3,13 @@ guid: 92aa81d6-b08e-4abb-ae39-ecbe5735a74c prebuilt: false platforms: Active Directory category: Dangerous Privileges -description: Creators of computer objects get abusable rights on the computer object. If the owner is not explicitly granted ReadLAPSPassword they can still compromise the computer with the abusable owner rights. +description: Creators of computer objects get abusable rights on the computer object. If the owner is not explicitly granted ReadLAPSPassword other permissions they can still compromise the computer with the abusable owner rights. query: |- - MATCH p = (c:Computer)<-[:GenericAll|Owns|WriteDacl|WriteOwner|AllExtendedRights]-(n:User) - WHERE c.haslaps = true AND c.ownersid = n.objectid + MATCH (c:Computer)<-[:Owns|OwnsLimitedRights]-(n:User) + WHERE c.haslaps = true + AND c.enabled = true RETURN p -revision: 1 +revision: 2 resources: acknowledgements: Martin Sohn Christensen, @martinsohndk From 739382f40aad4276e827fc25d60aef86383a7ca3 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Sun, 1 Nov 2026 10:10:34 +0100 Subject: [PATCH 07/55] Update Computer owners who can obtain LAPS passwords.yml --- queries/Computer owners who can obtain LAPS passwords.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/queries/Computer owners who can obtain LAPS passwords.yml b/queries/Computer owners who can obtain LAPS passwords.yml index d3bc8f0..a82579a 100644 --- a/queries/Computer owners who can obtain LAPS passwords.yml +++ b/queries/Computer owners who can obtain LAPS passwords.yml @@ -3,9 +3,9 @@ guid: 92aa81d6-b08e-4abb-ae39-ecbe5735a74c prebuilt: false platforms: Active Directory category: Dangerous Privileges -description: Creators of computer objects get abusable rights on the computer object. If the owner is not explicitly granted ReadLAPSPassword other permissions they can still compromise the computer with the abusable owner rights. +description: Creators of computer objects get abusable rights on the computer object. If the owner is not explicitly granted ReadLAPSPassword they can still compromise the computer with the abusable owner rights. query: |- - MATCH (c:Computer)<-[:Owns|OwnsLimitedRights]-(n:User) + MATCH p=(c:Computer)<-[:Owns|OwnsLimitedRights]-(n:User) WHERE c.haslaps = true AND c.enabled = true RETURN p From 2fc69b64949a8b23183afd7d7d79062e3912b2bc Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Sun, 1 Nov 2026 13:37:46 +0100 Subject: [PATCH 08/55] Add AD Site graph queries --- queries/Map AD site topology.yml | 14 ++++++++++++++ queries/Map site controllers.yml | 14 ++++++++++++++ queries/Map site servers to computers.yml | 13 +++++++++++++ queries/Map site-linked GPO controllers.yml | 14 ++++++++++++++ queries/Map site-linked GPOs.yml | 14 ++++++++++++++ 5 files changed, 69 insertions(+) create mode 100644 queries/Map AD site topology.yml create mode 100644 queries/Map site controllers.yml create mode 100644 queries/Map site servers to computers.yml create mode 100644 queries/Map site-linked GPO controllers.yml create mode 100644 queries/Map site-linked GPOs.yml diff --git a/queries/Map AD site topology.yml b/queries/Map AD site topology.yml new file mode 100644 index 0000000..66c3cdc --- /dev/null +++ b/queries/Map AD site topology.yml @@ -0,0 +1,14 @@ +name: Map AD site topology +guid: 23665310-99fc-4a14-b00c-1739cad3a6be +prebuilt: true +platforms: Active Directory +category: AD Sites +description: Shows sites and the site-server or site-subnet objects they contain. +query: |- + MATCH p = (:Site)-[:Contains]->(n:Base) + WHERE n:SiteServer OR n:SiteSubnet + RETURN p + LIMIT 1000 +revision: 1 +resources: +acknowledgements: diff --git a/queries/Map site controllers.yml b/queries/Map site controllers.yml new file mode 100644 index 0000000..5b35292 --- /dev/null +++ b/queries/Map site controllers.yml @@ -0,0 +1,14 @@ +name: Map site controllers +guid: d7d7e8c6-971c-48ca-a77e-92228ed01402 +prebuilt: true +platforms: Active Directory +category: AD Sites +description: Shows sites, their controllers, and the site-server or site-subnet objects they contain. +query: |- + MATCH p = (:Base)-[:Owns|WriteOwner|WriteDacl|GenericAll|GenericWrite|WriteGPLink]->(:Site)-[:Contains]->(n:Base) + WHERE n:SiteServer OR n:SiteSubnet + RETURN p + LIMIT 1000 +revision: 1 +resources: +acknowledgements: diff --git a/queries/Map site servers to computers.yml b/queries/Map site servers to computers.yml new file mode 100644 index 0000000..4938086 --- /dev/null +++ b/queries/Map site servers to computers.yml @@ -0,0 +1,13 @@ +name: Map site servers to computers +guid: aacb8a10-e2ab-4ba1-a6c5-f2896502299e +prebuilt: true +platforms: Active Directory +category: AD Sites +description: Shows SiteServer nodes and the Computer objects they resolve to through ServerIs. +query: |- + MATCH p = (:Site)-[:Contains]->(:SiteServer)-[:ServerIs]->(:Computer) + RETURN p + LIMIT 1000 +revision: 1 +resources: +acknowledgements: diff --git a/queries/Map site-linked GPO controllers.yml b/queries/Map site-linked GPO controllers.yml new file mode 100644 index 0000000..ad87ccb --- /dev/null +++ b/queries/Map site-linked GPO controllers.yml @@ -0,0 +1,14 @@ +name: Map site-linked GPO controllers +guid: 3fa47036-3999-410a-834c-680ec8dfdd0b +prebuilt: true +platforms: Active Directory +category: AD Sites +description: Shows site-linked GPO controllers and affected site-servers and site-subnets +query: |- + MATCH p = (:Base)-[:Owns|WriteOwner|WriteDacl|GenericAll|GenericWrite]->(:GPO)-[:GPLink]->(:Site)-[:Contains]->(n:Base) + WHERE n:SiteServer OR n:SiteSubnet + RETURN p + LIMIT 1000 +revision: 1 +resources: +acknowledgements: diff --git a/queries/Map site-linked GPOs.yml b/queries/Map site-linked GPOs.yml new file mode 100644 index 0000000..783e377 --- /dev/null +++ b/queries/Map site-linked GPOs.yml @@ -0,0 +1,14 @@ +name: Map site-linked GPOs +guid: 5bd38b39-bdc9-4f4c-9463-fe0631904fe7 +prebuilt: true +platforms: Active Directory +category: AD Sites +description: Shows site-linked GPOs and affected site-servers and site-subnets +query: |- + MATCH p = (:GPO)-[:GPLink]->(:Site)-[:Contains]->(n:Base) + WHERE n:SiteServer OR n:SiteSubnet + RETURN p + LIMIT 1000 +revision: 1 +resources: +acknowledgements: From 44beb82df445316f661c108344540fa881f71efa Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Sun, 1 Nov 2026 20:43:46 +0100 Subject: [PATCH 09/55] Align 'Users which do not require password to authenticate' with BH built-in. Query diverted from BH without. Additionally: TDOs are not node in BH. Condition contained error. --- .../Users which do not require password to authenticate.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/queries/Users which do not require password to authenticate.yml b/queries/Users which do not require password to authenticate.yml index 1cd4824..5a0b461 100644 --- a/queries/Users which do not require password to authenticate.yml +++ b/queries/Users which do not require password to authenticate.yml @@ -7,11 +7,9 @@ description: query: |- MATCH (u:User) WHERE u.passwordnotreqd = true - AND u.enabled = true - // Exclude interdomain trust accounts via UAC bit (0x0800 = 2048) - AND NOT u.useraccountcontrol % 4096 < 2048 RETURN u + RETURN u LIMIT 100 -revision: 2 +revision: 3 resources: acknowledgements: From fb2917e30a841b6eb174d17bb30c34df901dc822 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Mon, 2 Nov 2026 22:27:03 +0100 Subject: [PATCH 10/55] Create Accounts with local Administrator access to Tier Zero computers.yml --- docs/security-assessment-mapping.json | 19 +++++++++++++++++++ ...istrator access to Tier Zero computers.yml | 14 ++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 queries/Accounts with local Administrator access to Tier Zero computers.yml diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index d08345c..9c9866c 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -3277,6 +3277,25 @@ } ] }, + { + "bloodhound_query": { + "guid": "52490ad1-42b8-416b-ac38-4a45cbe4d73c", + "name": "Accounts local Administrator of Tier Zero computers" + }, + "maps_to": [ + { + "source": "MDI", + "controls": [ + { + "mapping_scope": "exact", + "mapping_scope_detail": "", + "id": "Remove local admins on identity assets", + "name": "Remove local admins on identity assets" + } + ] + } + ] + }, { "bloodhound_query": { "guid": "d65a801f-d3ef-4b7e-8030-99ebfd6dad12", diff --git a/queries/Accounts with local Administrator access to Tier Zero computers.yml b/queries/Accounts with local Administrator access to Tier Zero computers.yml new file mode 100644 index 0000000..d228fea --- /dev/null +++ b/queries/Accounts with local Administrator access to Tier Zero computers.yml @@ -0,0 +1,14 @@ +name: Accounts local Administrator of Tier Zero computers +guid: 52490ad1-42b8-416b-ac38-4a45cbe4d73c +prebuilt: false +platforms: Active Directory +category: Dangerous Privileges +description: +query: |- + MATCH p=()-[:MemberOf*0..]->()-[:AdminTo]->(n:Computer) + WHERE (n:Tag_Tier_Zero) + RETURN p + LIMIT 1000 +revision: 1 +resources: +acknowledgements: From f7afb01cbd61a211581080864bdc8cc1ffb8bba6 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Mon, 2 Nov 2026 22:27:43 +0100 Subject: [PATCH 11/55] Create Accounts with possible secrets in description.yml --- docs/security-assessment-mapping.json | 19 +++++++++++++++++++ ...s with possible secrets in description.yml | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 queries/Accounts with possible secrets in description.yml diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index 9c9866c..22b36e8 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -225,6 +225,25 @@ } ] }, + { + "bloodhound_query": { + "guid": "b1c2fc7d-61fd-4047-81dc-42e75bea35b1", + "name": "Accounts with possible secrets in description" + }, + "maps_to": [ + { + "source": "MDI", + "controls": [ + { + "mapping_scope": "partial", + "mapping_scope_detail": "Query searches limited attributes and does not use GenAI-powered analysis.", + "id": "Remove discoverable passwords in Active Directory account attributes", + "name": "Remove discoverable passwords in Active Directory account attributes" + } + ] + } + ] + }, { "bloodhound_query": { "guid": "5862dc4e-6f6f-4321-9474-d838968495ed", diff --git a/queries/Accounts with possible secrets in description.yml b/queries/Accounts with possible secrets in description.yml new file mode 100644 index 0000000..ff85222 --- /dev/null +++ b/queries/Accounts with possible secrets in description.yml @@ -0,0 +1,18 @@ +name: Accounts with possible secrets in description +guid: b1c2fc7d-61fd-4047-81dc-42e75bea35b1 +prebuilt: false +platforms: +- Active Directory +- Azure +category: Active Directory Hygiene +description: Users and computers whose description contains secret/password-related keywords. Broad match, so expect false positives. +query: |- + MATCH (n) + WHERE (n:User OR n:Computer) + AND n.description IS NOT NULL + AND n.description =~ '(?i).*(\\bpass(word|wd|phrase|code)?s?\\b|\\bpwd\\b|\\bpw\\b|p[@a]ssw[o0]rd|\\bcred(ential)?s?\\b|\\bsecret\\b|\\bpsk\\b|\\bpin\\b|temp(orary)?\\s?(pw|pass)|initial\\s?(pw|pass)|default\\s?(pw|pass)|reset\\s?to).*' + RETURN n + LIMIT 1000 +revision: 1 +resources: +acknowledgements: From 4fac43df6980f081d93d45fdab350f252c048650 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Mon, 2 Nov 2026 22:28:23 +0100 Subject: [PATCH 12/55] Create Hybrid accounts Tier Zero in both Entra ID and Active Directory --- docs/security-assessment-mapping.json | 19 +++++++++++++++++++ ... in both Entra ID and Active Directory.yml | 15 +++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 queries/Hybrid accounts Tier Zero in both Entra ID and Active Directory.yml diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index 22b36e8..c3e5f2b 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -244,6 +244,25 @@ } ] }, + { + "bloodhound_query": { + "guid": "372579b2-a671-4697-a771-a9d32c5c61da", + "name": "Hybrid accounts Tier Zero in both Entra ID and Active Directory" + }, + "maps_to": [ + { + "source": "MDI", + "controls": [ + { + "mapping_scope": "superset", + "mapping_scope_detail": "Expanded scope to Tier Zero", + "id": "Microsoft Entra ID privileged user accounts that are also privileged in Active Directory", + "name": "Microsoft Entra ID privileged user accounts that are also privileged in Active Directory" + } + ] + } + ] + }, { "bloodhound_query": { "guid": "5862dc4e-6f6f-4321-9474-d838968495ed", diff --git a/queries/Hybrid accounts Tier Zero in both Entra ID and Active Directory.yml b/queries/Hybrid accounts Tier Zero in both Entra ID and Active Directory.yml new file mode 100644 index 0000000..428f4db --- /dev/null +++ b/queries/Hybrid accounts Tier Zero in both Entra ID and Active Directory.yml @@ -0,0 +1,15 @@ +name: Hybrid accounts Tier Zero in both Entra ID and Active Directory +guid: 372579b2-a671-4697-a771-a9d32c5c61da +prebuilt: false +platforms: +- Active Directory +- Azure +category: Cross Platform Attack Paths +description: +query: |- + MATCH p = (:Tag_Tier_Zero)-[:SyncedToADUser|SyncedToEntraUser]->(:Tag_Tier_Zero) + RETURN p + LIMIT 1000 +revision: 1 +resources: +acknowledgements: From c54c792bddbf8561cb4a50c45b869d835a25e546 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Mon, 2 Nov 2026 22:28:48 +0100 Subject: [PATCH 13/55] Create Non-Tier Zero accounts with WriteDACL on Tier Zero group --- docs/security-assessment-mapping.json | 19 +++++++++++++++++++ ...unts with WriteDACL on Tier Zero group.yml | 15 +++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 queries/Non-Tier Zero accounts with WriteDACL on Tier Zero group.yml diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index c3e5f2b..9a42f06 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -3625,6 +3625,25 @@ } ] }, + { + "bloodhound_query": { + "guid": "d9e80a24-93a8-4847-b354-762b7ac7510d", + "name": "Non-Tier Zero accounts with WriteDACL on Tier Zero group" + }, + "maps_to": [ + { + "source": "MDI", + "controls": [ + { + "mapping_scope": "superset", + "mapping_scope_detail": "Expanded scope to Tier Zero", + "id": "Ensure non-admin identities cannot have WriteDACL permissions on sensitive groups", + "name": "Ensure non-admin identities cannot have WriteDACL permissions on sensitive groups" + } + ] + } + ] + }, { "bloodhound_query": { "guid": "43481dea-6c9c-47f0-8a3a-0c012c1a811e", diff --git a/queries/Non-Tier Zero accounts with WriteDACL on Tier Zero group.yml b/queries/Non-Tier Zero accounts with WriteDACL on Tier Zero group.yml new file mode 100644 index 0000000..bb28aa9 --- /dev/null +++ b/queries/Non-Tier Zero accounts with WriteDACL on Tier Zero group.yml @@ -0,0 +1,15 @@ +name: Non-Tier Zero accounts with WriteDACL on Tier Zero group +guid: d9e80a24-93a8-4847-b354-762b7ac7510d +prebuilt: false +platforms: Active Directory +category: Dangerous Privileges +description: +query: |- + MATCH p=(n:Base)-[:WriteDACL]->(g:Group) + WHERE NOT (n:Tag_Tier_Zero) + AND (g:Tag_Tier_Zero) + RETURN p +revision: 1 +resources: +acknowledgements: Martin Sohn Christensen, @martinsohndk + From 1f8d331dff6df5e09cb35386336d55e8d70b8fe6 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Mon, 2 Nov 2026 22:29:14 +0100 Subject: [PATCH 14/55] LIMIT 1000 on Accounts with weak password storage encryption.yml --- queries/Accounts with weak password storage encryption.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/queries/Accounts with weak password storage encryption.yml b/queries/Accounts with weak password storage encryption.yml index 3f7f543..9e53bab 100644 --- a/queries/Accounts with weak password storage encryption.yml +++ b/queries/Accounts with weak password storage encryption.yml @@ -18,8 +18,8 @@ query: |- AND NOT n.whencreated IN [-1.0, 0.0] ) RETURN n - LIMIT 100 -revision: 4 + LIMIT 1000 +revision: 5 resources: - https://techcommunity.microsoft.com/blog/coreinfrastructureandsecurityblog/decrypting-the-selection-of-supported-kerberos-encryption-types/1628797 - https://learn.microsoft.com/en-us/windows-server/security/kerberos/detect-remediate-rc4-kerberos From a0c9002bb55510d8f826410f7aab63446afdb038 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Mon, 2 Nov 2026 22:29:52 +0100 Subject: [PATCH 15/55] Recursive memberships on All members of Operator groups --- queries/All members of Operator groups.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/queries/All members of Operator groups.yml b/queries/All members of Operator groups.yml index 147ee5f..7b0bf02 100644 --- a/queries/All members of Operator groups.yml +++ b/queries/All members of Operator groups.yml @@ -5,7 +5,7 @@ platforms: Active Directory category: Domain Information description: query: |- - MATCH p=(:Base)-[:MemberOf]->(n:Group) + MATCH p=(:Base)-[:MemberOf*1..]->(n:Group) WHERE ( n.objectid ENDS WITH 'S-1-5-32-551' OR // Backup Operators n.objectid ENDS WITH 'S-1-5-32-556' OR // Network Configuration Operators @@ -16,7 +16,8 @@ query: |- n.objectid ENDS WITH 'S-1-5-32-550' // Print Operators ) RETURN p -revision: 2 + LIMIT 1000 +revision: 3 resources: acknowledgements: Martin Sohn Christensen, @martinsohndk From 3e5c5d5a98fbf6f8311457bf0f0d5d0c6644af0b Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Mon, 2 Nov 2026 22:30:09 +0100 Subject: [PATCH 16/55] Change to Role GUID matching in All privileged Azure Service Principals --- ...ll privileged Azure Service Principals.yml | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/queries/All privileged Azure Service Principals.yml b/queries/All privileged Azure Service Principals.yml index 19ec4c1..738dc39 100644 --- a/queries/All privileged Azure Service Principals.yml +++ b/queries/All privileged Azure Service Principals.yml @@ -6,9 +6,25 @@ platforms: category: Azure Hygiene description: Return all privileged Azure Service Principals. query: |- - MATCH p=(n:AZServicePrincipal)-[:AZHasRole|AZMemberOf*1..2]->(r:AZRole) - WHERE r.displayname =~ '(?i)Global Administrator|User Administrator|Cloud Application Administrator|Authentication Policy Administrator|Exchange Administrator|Helpdesk Administrator|PRIVILEGED AUTHENTICATION ADMINISTRATOR|Domain Name Administrator|Hybrid Identity Administrator|External Identity Provider Administrator|Privileged Role Administrator|Partner Tier2 Support|Application Administrator|Directory Synchronization Accounts' + MATCH p=(n:AZServicePrincipal)-[:AZMemberOf*1..]->(:AZGroup)-[:AZHasRole]->(r:AZRole) + WHERE toUpper(r.roledefinitionid) IN [ + '62E90394-69F5-4237-9190-012177145E10', // Global Administrator + 'FE930BE7-5E62-47DB-91AF-98C3A49A38B1', // User Administrator + '158C047A-C907-4556-B7EF-446551A6B5F7', // Cloud Application Administrator + '0526716B-113D-4C15-B2C8-68E3C22B9F80', // Authentication Policy Administrator + '29232CDF-9323-42FD-ADE2-1D097AF3E4DE', // Exchange Administrator + '729827E3-9C14-49F7-BB1B-9608F156BBB8', // Helpdesk Administrator + '7BE44C8A-ADAF-4E2A-84D6-AB2649E08A13', // Privileged Authentication Administrator + '8329153B-31D0-4727-B945-745EB3BC5F31', // Domain Name Administrator + '8AC3FC64-6ECA-42EA-9E69-59F4C7B60EB2', // Hybrid Identity Administrator + 'BE2F45A1-457D-42AF-A067-6EC1FA63BC45', // External Identity Provider Administrator + 'E8611AB8-C189-46E8-94E1-60213AB1F814', // Privileged Role Administrator + 'E00E864A-17C5-4A4B-9C06-F5B95A8D5BD8', // Partner Tier2 Support + '9B895D92-2CD3-44C7-9D02-A6AC2D5EA5C3', // Application Administrator + 'D29B2B05-8046-44BA-8758-1E26182FCF32' // Directory Synchronization Accounts + ] RETURN p -revision: 1 + LIMIT 1000 +revision: 2 resources: acknowledgements: Daniel Scheidt, @theluemmel From 788ae335797da95559d98b277bb1b64f31c2f0df Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Mon, 2 Nov 2026 22:30:49 +0100 Subject: [PATCH 17/55] Update description on Enrollment rights on published ESC15 certificate templates --- ...llment rights on published ESC15 certificate templates.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/queries/Enrollment rights on published ESC15 certificate templates.yml b/queries/Enrollment rights on published ESC15 certificate templates.yml index 2897a34..2229143 100644 --- a/queries/Enrollment rights on published ESC15 certificate templates.yml +++ b/queries/Enrollment rights on published ESC15 certificate templates.yml @@ -3,7 +3,7 @@ guid: 78d59fe1-e1a0-4813-adc9-c3c96ac08b66 prebuilt: false platforms: Active Directory category: Active Directory Certificate Services -description: Enrollment rights on certificate templates that meet the requirements for the ADCS ESC15 (EKUwu) attack. +description: Enrollment rights on certificate templates that meet the requirements for the ADCS ESC15 (EKUwu) attack, addressed by CVE-2024-49019. query: |- MATCH p=(:Base)-[:Enroll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(:EnterpriseCA)-[:TrustedForNTAuth]->(:NTAuthStore)-[:NTAuthStoreFor]->(:Domain) WHERE ct.enrolleesuppliessubject = True @@ -11,7 +11,7 @@ query: |- AND ct.requiresmanagerapproval = False AND ct.schemaversion = 1 RETURN p -revision: 1 +revision: 2 resources: - https://x.com/SpecterOps/status/1844800558151901639 - https://msrc.microsoft.com/update-guide/en-US/advisory/CVE-2024-49019 From 4ceb67ccaa058a927d0acb45a67d3b5195cb9c56 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Mon, 2 Nov 2026 22:31:25 +0100 Subject: [PATCH 18/55] Add guest primary group in scope of Users with non-default Primary Group membership --- ...h non-default Primary Group membership.yml | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/queries/Users with non-default Primary Group membership.yml b/queries/Users with non-default Primary Group membership.yml index 190f7aa..bdb0122 100644 --- a/queries/Users with non-default Primary Group membership.yml +++ b/queries/Users with non-default Primary Group membership.yml @@ -6,13 +6,25 @@ category: Active Directory Hygiene description: query: |- MATCH p=(n:User)-[r:MemberOf]->(g:Group) - WHERE NOT g.objectid ENDS WITH "-513" // Domain Users - AND r.isprimarygroup = true - AND NOT n.objectid ENDS WITH "-501" // Guests account, as it has primaryGroup to Guests - AND (n.gmsa IS NULL OR n.gmsa = false) // Not gMSA, as it has primaryGroup to Domain Computers - AND (n.msa IS NULL OR n.msa = false) // Not MSA, as it has primaryGroup to Domain Computers + WHERE + ( + ( + r.isprimarygroup = false + AND n.objectid ENDS WITH "-501" // User: Guest + AND g.objectid ENDS WITH "-514" // Group: Domain Guests + ) + OR + ( + r.isprimarygroup = true + AND NOT g.objectid ENDS WITH "-513" // Group: Domain Users + AND NOT n.objectid ENDS WITH "-501" // User: Guest + ) + AND (n.gmsa IS NULL OR n.gmsa = false) // Not gMSA, as it has primaryGroup to Domain Computers + AND (n.msa IS NULL OR n.msa = false) // Not MSA, as it has primaryGroup to Domain Computers + ) RETURN p -revision: 1 + LIMIT 1000 +revision: 2 resources: acknowledgements: Martin Sohn Christensen, @martinsohndk From b12b55dcb6c7ec94284ecae5ace2bd261e46e974 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Mon, 2 Nov 2026 22:31:39 +0100 Subject: [PATCH 19/55] Update security-assessment-mapping.json --- docs/security-assessment-mapping.json | 253 ++++++++++++++++++++------ 1 file changed, 197 insertions(+), 56 deletions(-) diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index 9a42f06..cec9549 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -203,6 +203,23 @@ "name": "[M]Inactive account check" } ] + }, + { + "source": "MDI", + "controls": [ + { + "mapping_scope": "partial", + "mapping_scope_detail": "Query does not exclude service accounts.", + "id": "Remove stale Active Directory accounts", + "name": "Remove stale Active Directory accounts" + }, + { + "mapping_scope": "partial", + "mapping_scope_detail": "Query does not exclude non-service accounts.", + "id": "Remove Stale Service Accounts", + "name": "Remove Stale Service Accounts" + } + ] } ] }, @@ -448,8 +465,8 @@ { "mapping_scope": "exact", "mapping_scope_detail": "", - "id": "Unsecure account attributes: Remove Do not require Kerberos preauthentication", - "name": "Unsecure account attributes: Remove Do not require Kerberos preauthentication" + "id": "Unsecure account attributes: Remove Don't require Kerberos preauthentication", + "name": "Unsecure account attributes: Remove Don't require Kerberos preauthentication" } ] }, @@ -789,8 +806,8 @@ "source": "PingCastle", "controls": [ { - "mapping_scope": "partial", - "mapping_scope_detail": "PC flags service accounts not supporting AES. Following the RC4 disablement enforcement of CVE-2026-20833, the Key Distribution Center no longer assumes RC4 support for accounts without a msDS-SupportedEncryptionTypes value, so the BH query instead flags accounts where RC4 is explicitly permitted.", + "mapping_scope": "exact", + "mapping_scope_detail": "PC flags service accounts without AES flag. Following the RC4 disablement enforcement of CVE-2026-20833, the Key Distribution Center no longer assumes RC4 support for accounts without a msDS-SupportedEncryptionTypes value, so the BH query instead flags accounts where RC4 is explicitly permitted thus covers the same risk.", "id": "S-AesNotEnabled", "name": "[T]Check the use of Kerberos on services accounts without AES support" } @@ -800,8 +817,8 @@ "source": "MDI", "controls": [ { - "mapping_scope": "partial", - "mapping_scope_detail": "MDI flags accounts not supporting AES. Following the RC4 disablement enforcement of CVE-2026-20833, the Key Distribution Center no longer assumes RC4 support for accounts without a msDS-SupportedEncryptionTypes value, so the BH query instead flags accounts where RC4 is explicitly permitted.", + "mapping_scope": "exact", + "mapping_scope_detail": "MDI flags service accounts without AES flag. Following the RC4 disablement enforcement of CVE-2026-20833, the Key Distribution Center no longer assumes RC4 support for accounts without a msDS-SupportedEncryptionTypes value, so the BH query instead flags accounts where RC4 is explicitly permitted thus covers the same risk.", "id": "Unsecure account attributes: Enable Kerberos AES encryption support", "name": "Unsecure account attributes: Enable Kerberos AES encryption support" } @@ -900,17 +917,6 @@ } ] }, - { - "source": "MDI", - "controls": [ - { - "mapping_scope": "exact", - "mapping_scope_detail": "", - "id": "Weak cipher usage", - "name": "Weak cipher usage" - } - ] - }, { "source": "PurpleKnight", "controls": [ @@ -943,6 +949,63 @@ } ] }, + { + "bloodhound_query": { + "guid": "3f0fa2f3-fbdf-42c0-9e7d-97e689009161", + "name": "Collection health of DC Registry Data" + }, + "maps_to": [ + { + "source": "", + "controls": [ + { + "mapping_scope": "partial", + "mapping_scope_detail": "MDI checks whether its sensor is installed on the server. This mapping checks whether the server is represented in BloodHound with the required supported data, which indicates BloodHound coverage. It does not verify MDI sensor deployment.", + "id": "Unmonitored domain controllers", + "name": "Unmonitored domain controllers" + } + ] + } + ] + }, + { + "bloodhound_query": { + "guid": "c8dd3479-8063-450a-9456-557bc5f39e10", + "name": "Collection health of CA Registry Data" + }, + "maps_to": [ + { + "source": "", + "controls": [ + { + "mapping_scope": "partial", + "mapping_scope_detail": "MDI checks whether its sensor is installed on the server. This mapping checks whether the server is represented in BloodHound with the required supported data, which indicates BloodHound coverage. It does not verify MDI sensor deployment.", + "id": "Unmonitored ADCS servers", + "name": "Unmonitored ADCS servers" + } + ] + } + ] + }, + { + "bloodhound_query": { + "guid": "c8dd3479-8063-450a-9456-557bc5f39e10", + "name": "Collection health of DC Registry Data" + }, + "maps_to": [ + { + "source": "", + "controls": [ + { + "mapping_scope": "partial", + "mapping_scope_detail": "MDI checks whether its sensor is installed on the server. This mapping checks whether the server is represented in BloodHound with the required supported data, which indicates BloodHound coverage. It does not verify MDI sensor deployment.", + "id": "Unmonitored ADFS servers", + "name": "Unmonitored ADFS servers" + } + ] + } + ] + }, { "bloodhound_query": { "guid": "421921fa-bc0f-4659-9680-b7481adcb132", @@ -966,8 +1029,8 @@ { "mapping_scope": "exact", "mapping_scope_detail": "", - "id": "Unsecure domain configurations: ms-DS-MachineAccountQuota", - "name": "Unsecure domain configurations: ms-DS-MachineAccountQuota" + "id": "Unsecure domain configurations: Set ms-DS-MachineAccountQuota to \"0\"", + "name": "Unsecure domain configurations: Set ms-DS-MachineAccountQuota to \"0\"" } ] }, @@ -1013,8 +1076,8 @@ "source": "PingCastle", "controls": [ { - "mapping_scope": "exact", - "mapping_scope_detail": "", + "mapping_scope": "superset", + "mapping_scope_detail": "Expanded scope to Tier Zero", "id": "P-Delegated", "name": "[M]At least one administrator account can be delegated" } @@ -1024,8 +1087,8 @@ "source": "MDI", "controls": [ { - "mapping_scope": "exact", - "mapping_scope_detail": "", + "mapping_scope": "superset", + "mapping_scope_detail": "Expanded scope to Tier Zero", "id": "Ensure privileged accounts are not delegated", "name": "Ensure privileged accounts are not delegated" } @@ -1055,23 +1118,12 @@ "controls": [ { "mapping_scope": "superset", - "mapping_scope_detail": "Expanded scope to Tier Zero", + "mapping_scope_detail": "", "id": "https://www.tenable.com/plugins/nessus/150480", "name": "Kerberoasting" } ] }, - { - "source": "MDI", - "controls": [ - { - "mapping_scope": "superset", - "mapping_scope_detail": "Expanded scope to Tier Zero", - "id": "Unsecure account attributes: Remove a Service Principal Name (SPN)", - "name": "Unsecure account attributes: Remove a Service Principal Name (SPN)" - } - ] - }, { "source": "PurpleKnight", "controls": [ @@ -1101,6 +1153,17 @@ "name": "Users with SPN defined" } ] + }, + { + "source": "MDI", + "controls": [ + { + "mapping_scope": "exact", + "mapping_scope_detail": "", + "id": "Unsecure account attributes: Remove a Service Principal Name (SPN)", + "name": "Unsecure account attributes: Remove a Service Principal Name (SPN)" + } + ] } ] }, @@ -1248,8 +1311,8 @@ "source": "MDI", "controls": [ { - "mapping_scope": "exact", - "mapping_scope_detail": "", + "mapping_scope": "partial", + "mapping_scope_detail": "Query infers lack the AES encryption keys by password reset time or account creation instead of reading NTDS.", "id": "Unsecure account attributes: Remove Password stored with weak encryption", "name": "Unsecure account attributes: Remove Password stored with weak encryption" } @@ -1465,8 +1528,8 @@ { "mapping_scope": "superset", "mapping_scope_detail": "Remove false positives by reducing scope to Non-Tier Zero", - "id": "Admin SDHolder permissions", - "name": "Admin SDHolder permissions" + "id": "Remove access rights on suspicious accounts with the Admin SDHolder permission", + "name": "Remove access rights on suspicious accounts with the Admin SDHolder permission" } ] }, @@ -1587,6 +1650,12 @@ "mapping_scope_detail": "", "id": "P-OperatorsEmpty", "name": "[M]Check that the operator groups are empty" + }, + { + "mapping_scope": "exact", + "mapping_scope_detail": "", + "id": "Locate accounts in built-in Operator Groups", + "name": "Locate accounts in built-in Operator Groups" } ] }, @@ -1911,6 +1980,12 @@ "mapping_scope_detail": "Expanded account scope to include accounts with SPN. Expanded group scope to include Enterprise Admins and Schema Admins", "id": "P-ServiceDomainAdmin", "name": "[M][T]Check if Service Accounts are domain administrators" + }, + { + "mapping_scope": "exact", + "mapping_scope_detail": "", + "id": "Identify service accounts in privileged groups", + "name": "Identify service accounts in privileged groups" } ] } @@ -2212,8 +2287,8 @@ { "mapping_scope": "exact", "mapping_scope_detail": "", - "id": "Overly permissive certificate template with privileged EKU (ESC2)", - "name": "Overly permissive certificate template with privileged EKU (ESC2)" + "id": "Edit overly permissive certificate template with privileged EKU (Any purpose EKU or No EKU) (ESC2)", + "name": "Edit overly permissive certificate template with privileged EKU (Any purpose EKU or No EKU) (ESC2)" } ] } @@ -2260,38 +2335,38 @@ { "mapping_scope": "exact", "mapping_scope_detail": "", - "id": "Prevent requests for certificates valid for arbitrary users (ESC1)", - "name": "Prevent requests for certificates valid for arbitrary users (ESC1)" + "id": "Prevent users to request a certificate valid for arbitrary users based on the certificate template (ESC1)", + "name": "Prevent users to request a certificate valid for arbitrary users based on the certificate template (ESC1)" }, { "mapping_scope": "exact", "mapping_scope_detail": "", - "id": "Misconfigured enrollment agent certificate template (ESC3)", - "name": "Misconfigured enrollment agent certificate template (ESC3)" + "id": "Edit misconfigured enrollment agent certificate template (ESC3)", + "name": "Edit misconfigured enrollment agent certificate template (ESC3)" }, { "mapping_scope": "exact", "mapping_scope_detail": "", - "id": "Misconfigured certificate templates ACL (ESC4)", - "name": "Misconfigured certificate templates ACL (ESC4)" + "id": "Edit misconfigured certificate templates ACL (ESC4)", + "name": "Edit misconfigured certificate templates ACL (ESC4)" }, { "mapping_scope": "exact", "mapping_scope_detail": "", - "id": "Misconfigured certificate templates owner (ESC4)", - "name": "Misconfigured certificate templates owner (ESC4)" + "id": "Edit misconfigured certificate templates owner (ESC4)", + "name": "Edit misconfigured certificate templates owner (ESC4)" }, { "mapping_scope": "exact", "mapping_scope_detail": "", - "id": "Vulnerable Certificate Authority setting (ESC6)", - "name": "Vulnerable Certificate Authority setting (ESC6)" + "id": "Edit vulnerable Certificate Authority setting (ESC6)", + "name": "Edit vulnerable Certificate Authority setting (ESC6)" }, { "mapping_scope": "exact", "mapping_scope_detail": "", - "id": "Insecure ADCS certificate enrollment IIS endpoints (ESC8)", - "name": "Insecure ADCS certificate enrollment IIS endpoints (ESC8)" + "id": "Edit insecure ADCS certificate enrollment IIS endpoints (ESC8)", + "name": "Edit insecure ADCS certificate enrollment IIS endpoints (ESC8)" } ] }, @@ -2320,6 +2395,25 @@ } ] }, + { + "bloodhound_query": { + "guid": "77a708b8-962e-4c3d-ad70-e994126aaeba", + "name": "CA Administrators and CA Managers (ESC7)" + }, + "maps_to": [ + { + "source": "MDI", + "controls": [ + { + "mapping_scope": "exact", + "mapping_scope_detail": "", + "id": "Edit misconfigured Certificate Authority ACL (ESC7)", + "name": "Edit misconfigured Certificate Authority ACL (ESC7)" + } + ] + } + ] + }, { "bloodhound_query": { "guid": "96e70597-2d74-4503-a624-f1e30b642894", @@ -2505,8 +2599,8 @@ { "mapping_scope": "exact", "mapping_scope_detail": "", - "id": "Unsecure domain configurations: LDAP Signing", - "name": "Unsecure domain configurations: LDAP Signing" + "id": "Resolve unsecure domain configurations: Enforce LDAP Signing policy to \"Require signing\"", + "name": "Resolve unsecure domain configurations: Enforce LDAP Signing policy to \"Require signing\"" } ] }, @@ -2632,12 +2726,23 @@ "controls": [ { "mapping_scope": "partial", - "mapping_scope_detail": "Returns all principals with DCSync privileges.", + "mapping_scope_detail": "Returns all principals with DCSync privileges. In the results, look for accounts that are not Tier Zero.", "id": "Remove non-admin accounts with DCSync permissions", "name": "Remove non-admin accounts with DCSync permissions" } ] }, + { + "source": "MDI", + "controls": [ + { + "mapping_scope": "partial", + "mapping_scope_detail": "Returns all principals with DCSync privileges. In the results, look for User accounts prefixed with 'MSOL_'.", + "id": "Remove unnecessary replication permissions for Microsoft Entra Connect AD DS Connector account", + "name": "Remove unnecessary replication permissions for Microsoft Entra Connect AD DS Connector account" + } + ] + }, { "source": "PurpleKnight", "controls": [ @@ -3282,6 +3387,17 @@ "name": "Accounts with Kerberos constrained delegation configured to SSO computer account" } ] + }, + { + "source": "MDI", + "controls": [ + { + "mapping_scope": "exact", + "mapping_scope_detail": "", + "id": "Remove unsafe permissions on sensitive Microsoft Entra Connect accounts", + "name": "Remove unsafe permissions on sensitive Microsoft Entra Connect accounts" + } + ] } ] }, @@ -3582,6 +3698,12 @@ "mapping_scope_detail": "Expanded scope to Tier Zero", "id": "5c8bd887-55ac-47d6-9835-6ed5370cda26", "name": "Privileged group contains guest account" + }, + { + "mapping_scope": "superset", + "mapping_scope_detail": "Expanded scope to Tier Zero", + "id": "Ensure external and guest accounts are not granted privileged roles", + "name": "Ensure external and guest accounts are not granted privileged roles" } ] } @@ -3644,6 +3766,25 @@ } ] }, + { + "bloodhound_query": { + "guid": "92f269ee-3727-4ffa-947b-aad492ac0fa2", + "name": "All privileged Azure Service Principals" + }, + "maps_to": [ + { + "source": "MDI", + "controls": [ + { + "mapping_scope": "exact", + "mapping_scope_detail": "", + "id": "Ensure service accounts in Entra ID are not assigned Domain Name Admin or Global Admin roles", + "name": "Ensure service accounts in Entra ID are not assigned Domain Name Admin or Global Admin roles" + } + ] + } + ] + }, { "bloodhound_query": { "guid": "43481dea-6c9c-47f0-8a3a-0c012c1a811e", From fafae66a7bd14ce5ac02d0111563987d5e6b218e Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Mon, 2 Nov 2026 22:42:13 +0100 Subject: [PATCH 20/55] Update security-assessment-mapping.md --- docs/security-assessment-mapping.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/security-assessment-mapping.md b/docs/security-assessment-mapping.md index 20fdff5..e6490a2 100644 --- a/docs/security-assessment-mapping.md +++ b/docs/security-assessment-mapping.md @@ -13,11 +13,13 @@ The following shows which other security tools the mapping supports and the numb | Security Tool | Total Controls | Mapped Controls | Coverage | |---------------|-------------------|---------------|----------| -| [Netwrix PingCastle](https://www.pingcastle.com/PingCastleFiles/ad_hc_rules_list.html) | 186 | 105 | 56% | -| [Semperis PurpleKnight](https://www.semperis.com/purple-knight/security-indicators/) | 190 | 96 | 51% | -| [Microsoft Defender for Identity: Security Posture Assessment](https://learn.microsoft.com/en-us/defender-for-identity/security-assessment) | 45 | 35 | 78% | +| [Netwrix PingCastle](https://www.pingcastle.com/PingCastleFiles/ad_hc_rules_list.html) | 186 | 107 | 58% | +| [Semperis PurpleKnight](https://www.semperis.com/purple-knight/security-indicators/) | 190 | 97 | 51% | +| [Microsoft Defender for Identity: Security Posture Assessment](https://learn.microsoft.com/en-us/defender-for-identity/security-assessment) | 83 (+3*) | 44 | 55% | | [Tenable Nessus: Active Directory Starter Scan](https://www.tenable.com/blog/new-in-nessus-find-and-fix-these-10-active-directory-misconfigurations) | 10 | 10 | 100% | +* MDI lists 83 checks; three checks marked with an asterisk verify MDI sensor installation and are excluded from the coverage calculation. Coverage is calculated as 44 of the remaining 80 checks. + ## Mapping Structure Each mapping includes a type that describes the relationship: - `partial` - Query covers the core assessment control but with different approach/scope From 15e164b74232b9daaa997721e7a1580f63fde075 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Mon, 2 Nov 2026 23:23:00 +0100 Subject: [PATCH 21/55] Update security assessment mapping references --- README.md | 7 +++++-- docs/security-assessment-mapping.md | 11 +---------- queries.specterops.io-security-assessments.png | Bin 0 -> 34744 bytes 3 files changed, 6 insertions(+), 12 deletions(-) create mode 100644 queries.specterops.io-security-assessments.png diff --git a/README.md b/README.md index 80dd7aa..a6d52ad 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ The library contains queries that demonstrate BloodHound's versatility beyond tr - Cherry-picked community queries - SpecterOps-created queries BloodHound Enterprise customers found valuable - Community contributed queries (see [Contributing](#contributing)) -- Novel queries to further showcase BloodHound's security assessment capabilities (see [security-assessment-mapping.md](/docs/security-assessment-mapping.md)) +- Novel queries to further showcase BloodHound's [security assessment capabilities](#security-assessment-mapping). Individual query files are stored in [/queries](/queries/) as `.yml` and are automatically combined into a single Queries.json/Queries.zip as part of our [releases](https://github.com/SpecterOps/BloodHoundQueryLibrary/releases). @@ -72,7 +72,10 @@ Whenever new queries are added, the syntax is automatically validated, ensuring BloodHound queries in this library have been mapped to controls from common security assessment tools, demonstrating how BloodHound can validate findings typically associated with dedicated tools like PingCastle, Microsoft Defender for Identity, and Tenable Nessus. -For full coverage details and mapping structure, see [security-assessment-mapping.md](/docs/security-assessment-mapping.md). +Explore supported tools, mapped controls, and coverage statistics on the [Security Assessments page](https://queries.specterops.io/security-assessments). The page is backed by the repository's [mapping overview](docs/security-assessment-mapping.md) and [mapping data](docs/security-assessment-mapping.json). + + +![Security Assessments page screenshot](queries.specterops.io-security-assessments.png) ## Learning Cypher Queries diff --git a/docs/security-assessment-mapping.md b/docs/security-assessment-mapping.md index e6490a2..f12e6c3 100644 --- a/docs/security-assessment-mapping.md +++ b/docs/security-assessment-mapping.md @@ -9,16 +9,7 @@ The BloodHound-centric mapping data is available at [security-assessment-mapping ## Assessment Coverage Overview -The following shows which other security tools the mapping supports and the number of BloodHound queries in the BloodHound Query Library that correspond to controls performed by the tools. - -| Security Tool | Total Controls | Mapped Controls | Coverage | -|---------------|-------------------|---------------|----------| -| [Netwrix PingCastle](https://www.pingcastle.com/PingCastleFiles/ad_hc_rules_list.html) | 186 | 107 | 58% | -| [Semperis PurpleKnight](https://www.semperis.com/purple-knight/security-indicators/) | 190 | 97 | 51% | -| [Microsoft Defender for Identity: Security Posture Assessment](https://learn.microsoft.com/en-us/defender-for-identity/security-assessment) | 83 (+3*) | 44 | 55% | -| [Tenable Nessus: Active Directory Starter Scan](https://www.tenable.com/blog/new-in-nessus-find-and-fix-these-10-active-directory-misconfigurations) | 10 | 10 | 100% | - -* MDI lists 83 checks; three checks marked with an asterisk verify MDI sensor installation and are excluded from the coverage calculation. Coverage is calculated as 44 of the remaining 80 checks. +See the [Security Assessments page](https://queries.specterops.io/security-assessments) for supported security tools, mapped controls, and coverage statistics calculated from the mapping JSON on GitHub's `main` branch. You can search the mappings and explore the corresponding BloodHound queries there. ## Mapping Structure Each mapping includes a type that describes the relationship: diff --git a/queries.specterops.io-security-assessments.png b/queries.specterops.io-security-assessments.png new file mode 100644 index 0000000000000000000000000000000000000000..66122c66768aba31496fc50b7d923a397e2197c5 GIT binary patch literal 34744 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%0ATHV%lB&3 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�{^=w9zvh3c3b<`LsQ z2y#WDM)QO1?RP{y_FJNKin$%ql975{lqkG~Us-p16xS8Nm%QuA>^tA#HgckD>ciNG zfLin@pW4CXv8^?orG{st)bNzanLR`8tip}n_zZwW^jjuZq(8`zRonvj*`r{xN@*u^ zdNHv1zw#-G8m_Xcs^&gHfNBx#9foP?3Ab|RsmVIKCcXRD>(F9P&uG_+AMbzW=-N9! zMd{d6+sG1)hUf=v1hfZy&gT}Xh3wyMDC}wvrrQuRH%$5DP({k6B}RcUNRn196Tkb2 zu)~8M2F6g6&#oJAv=|1xD&Hs(`2Y}KL*k4TP%0NqHf}>5y0PLb#bYriV(iY6n=bDd z81hFaN(09nT^y|n3(J`=+w$H3m^kgwyQX#wZWL(Sm0J#vN+IV9_S?1iHApTTu`*4o zKQ{Z981%0$FmjE>J_nrY?@A3d*jLDrIr2nu|7QaxPOZmh`;O7$DDGzm0tl&sR#stH z^C`!9Jp7sVl;$@C&eyQa)MQm0Ix*s7&1N?N@(Zj4HUj0>hGWU@(^6uiGV>nCHR>n9 zHz+sO?XbiiO@#xBIci@A;n(L(;OFl%SfSuLbgubZ)rR*ICis!*4mB4feT00VQKGeT z>b|9B{pj7NBc5C0?d&~QiFzkb@2*H}>vw4I=Nm9BgP4VO^gHIakXjcnwwuVQ7R_Sn^sAoJa(b;2^^0F<<+XY8q!Sw+i*#Rra$&Zjbkh5oMYmm9dT!V3D?=p?k>YTp2T#2H!+p+B#Eqla z%&I%x+cVk6teZ_J^b4Tkj{SN;52l`VI!LAt#wJgcCp( zJqXti7lS?!&bfG*@laXvg-WXet#aMWl&iHX8mYC@-j2#{k}Cl3*yGi6F=yI~Js|1_ z9eexAE_Z=H(hRjT0%a#h!Jci$YLSNz025K!jitEG?fopUJ&+hG$;(a@OhK1wN-6?$ zG;mx*BRoa?(J>ADc2);beUPQw)E}egh=8+L}rS?P5TopMmOY z^58<5>*O6@Tn~$S|Hc`TZvyAuoTf`hCqdXYtL{FJ)DH31RMm+kP47!n<9*6ftxvZ2 zt!>)lox0&dGP{;semT2v`zCke<0_CJCumtb<V4Zf#vsounIss-}fw?aUI%2i<43$Yb>)+K66fbGV@UuxBE;Ke?O7OeiIN$h|>?e@E47rv=oO1Woe9 z#wm6-6`}D0-3N|nIgc`9@+0Ux4uoBd%wRxOrnNU^sqzS~{@Tacm^BL}=~Vi)!&*l?4Ys#A#5ex_exK!9_Ap^WK7oo(x?V|uE zIe)9N^$EKAUam=|qQ64gVx{`z2OgX^^Q`Q1>;P|{qZ4^8)45nRbqFFutE(($3|^~3M~RrK&=&cqy@3NS!R{+eR2 zgLBos_RfS6M&sC@(-}Np(Y1^ts*P40%qcni{|Eur07psJxLEwO7KPRMGBYg z`#g~xgHM*4H;BN{Xz@KC%o+SnEoo&cylDXf_Xt*Q2Tn@tUjuA^nC`srEpR)iJIVgR z!RK9uN^UJ|r#Y*n7Mj;|`FrW@>Ngm%lOf4N4}j4*8R{KSyI8r2y-h=E2ChEzgo@Od zhxQFqd0_qBg15pCoY4`xE-51tx485bdD&w$rL3@z6cj-e1wE7fgcNFfD|+d@@rtG0 z$!Ai$Mo2>swWF8Px)mUy3;i3~0$O+GyW>tswUs0>wNvGI zjYJiH4zbdz>`c*l4hWUG#)#?j-0BCnknKa>oeYVWmox&T3y!n=78Pn$QLV#`^2qVB zU0NV+I-+|Zx+C_B>)OrI2fY2@@>Ctrr=pWIao>SU=n0tFGT&EKSWf~SpGdZBPLw$A zZ8qe$X_DljO-;{&DF~H~9bCnK^dB#53$P_XwySV{_=ZKjq`m#=PwA;edL5qTfDmU2}=#BNA1u?!^T4pGZ=V~Iv+?Kn05J7!sxUq zZ%VFG?w5N~UP`;Sh#Qj4&h24ID}h{{8@$Pq6QHTwPP=RIb++Dn7mMk7tHpPsAV^tY z;V;gg7(%G^e!niGsMRF)yhhl!Utno1r7YmM#1j6?L z|CU<;n_%9*N-M6GS#1LVG6aCVPjZ}Npap({ki_FEEBPL!@2nT!!A?eU5M5Dn8d`R%SlcCe?^H|3Oci9c>D3711 zHQfZJn^t9k;m_P2>NggwQ$;YQH*U>pe)JbA6c$NNsdK#sEsRK4rnxce@ZiQJ0bbr3 z%W%2k#!l_ZHqGtDE3yrY3U0GMu3T}M-&{swRDl$bLu8(2=BDJ8uYdi1A>d-?KcyPL zztuwkq}TtEo(QTrMW?d9+{<*`i{_KVgMh|AW5Q@uH7!)_wDraUXhLvTzxqGQFitES zifq9D^7*tU7})`I+7YnhF}$@C>|}Uzlp)*o4RAs_t}`PS|%YlV*pbt@ZCJOAPmJ^@5?Rg zsG&Uh9~V-x?s4$(I1z5jn2uT-AsS=Q>9LYgQey1Pc*)1-9&D|?G{&OfkN!8sg!%m) zF@uoGMWnl4IVmiRBKp=mx3I$bs(VZ)nd&rt$+p^O#(v35|BA@h%p|`iw(-*5Lp=S4 zr|Y_7SnliCQG-rR*X5ebn=$@cjwrJd*X2iS?Ja{lk@|fPRvkyn;Kds77x39`o5Y*2 zWx%2c^xU`%!E8dp0l|Z$62Wg7MstBEEOzG_%G?3g3Ru5yUGO`bz_=P1axdi*9}rN; zTP(MQn%Pz-$n;X<({Q^@#Vv_;4=#GUKPEL4{6puEzhHg7(&n{@$-I-=O$m-ixdr-5 zG27*sM=Vpy2RW?0nfB&OfWql5w>8Hpp##+gzhf+294QJ7b+2k1KKS1{!~Xoj>Q&0J zzw3nzCP0%>+6P)+riPB?o+3(Fr)Qd;Gqk=DnIL*7P&Cs%U(>9%XDLUy8D>V(=&Fu5%O7-ytXAre+EZ0TVXN=NJoJ zWA}JCStbCT4fjQ-s?EsQ8OEW#X@Qv)<6X#y3nLMGEx{14XNA~O^x$c48Gy2ExZsrY zIhGoV8#+mbT;{!QS1Yj{US`A=Y;_;WDO!o>e|rJK7#fd5g$*bTH-r~8Csf=i=83?2 z7-ub5U%4Ek6<3^-z4!(>koW}e*LkQ6P+&OeI}(v;dpEUz2-EI!xE_~NoYom7Q}cR* zANLs8Wz#*ui_^auvp)QK-E-&TQEG16r@D=j;1K6K(-W$+xQwf7f> zE997+jNeW1XkOxNZ~swLu*Rj5eyCGL9@0NIE`>pu2Z6&|3kUOkg4lOSbq8+QgOU*& zZ~9rXMG}M_NN&BKRW!B!Jl}RKi1PVNXgxyHB!{cpl7(%ansSv$;3Hnv}%%88)pH8 zkLzrIS9GEu{+aVJk{RXHbVG2~G!Ar&bE#Y3So^;1x$%t;4}-ywPfc>D`e&8$fCuz8 zVMu(T)`H9ZXto8EdSdK5vV~HdpQf+7KbT262(vuXy!ocwtfa8JUq8b4QCNcT6dQ{&rN%) z+Fv(AiVisG+j!!>pT+n_%kr@}WcppP^6K5N?`>=3?qs+IY(Jw@`1rlf<0>e%_w+#~ zJ?H3A4u8F<&TNImF+%34g!dRROt8G4_9rBiRAtT6{VSix4WPW&7>TaA8itt@+!;py zu33SNs4HT;8Na{D29y;p(b2QJh!GF=yz7&t^EpnXyq%@%z;Xf>6yUU+;b3ow5ajL4 zR3_u5X1cdOB+%>an%&;^=U)OItHP{x!vU>DIHp&-YIXDFQO4CFm|k%_+L`Mo9CB0bB>oH%97dPi^OH~Fab<*Uj{OxeQ|rjBd~2X%ybGSF3s z+n778E3)5i4=n9BY(;6JeX_qjUv*uQcLh6W`zwl%rshxjFU@~RKc@eb^9Bat|8aAt z|HBXaJG7T36aCMeC7Q!!`fKd}+rIbLqX77d|C4rC$s5$Te?ebFjFs82(HR`Pr zIcseZoUqQis1%n$7#PspUY$x-n3&A$0CWp$QJ!t(an|iXOlVrM5yL#s(SH}y(tNPMbn{+fHy~fxQM@D2@+;vhQ{Arn zGGj=tnUC3}9=EfiV?c4(A9s>QfNi`dQ&tdo+b?x%ruyuJF+mwaD^9v5L_To~5c1M! zF#8sYoMgCROW3OV<(Iz;f8@;sq&vmj%_Z7OuRpm}%SaROPHE}#M*=P!2o{isj3?(= z_5TD6-+PaeikdGLt1A>6aB8Kn$6k=xOV0YS+f1>t#3R!j(%sma#obCxsdwz~B#(iZ zcu`Qa5g_I5cNql)wKsgL-5Tl|lCK>*r1?{Pd+&QY_|G|$Hka+ANCH9R9vZ;-IKo;@ ztM_Qyj_m&G4WhvJuK5Gf?LP_txIPhbm#b~-nS*P&H^d%YPI{)BK8+xCK3puFR!9UW zBL>Z`iaN~Vr%NK1j{}r`mzi04|L*>N=PUik`zepJ)JDaj6Q7I#=8ebg8m9a!M>IE+Rv)z=J2Yxp*moZkhnayhs=E^@2w<_Be$ME-E@ifn=N!m)fmvU;$(cE$<3~MSHf(Z zl_b{pVyA57&Y61*N3&6<<~)7`kvk{T%IbpyK4<8zcg?1`X0H%PAHA`si@6q9!KRlKDlK2=_E~4w=iuF@w zOU%Ouv=)LgP&J&0x{3v@O5vqm@La3rQuM}#VF}aTLW${m{eqB4cQMhSHUzoyY5hq( z6U1=W*C-WLm0BlPS|i4A%l&0Epbnj|;@DliT*FyA=Ad08gQ-S$%|P`{LxT2V2GMB( z2HFjbju|_Y5{y5T>ta;a%swdJQ-hWw?yh@>yz0yOCF3u4EA(B0MF*#OV;xYh$`zpN z>b^tuL_mnLH94VclDU)&y4&9=8U78oo$F5Q)cD^%$IP_@s%(2wyp4!^!QZb*do)|% z+u|{9bR#?k|F=H$u;}a^bcF$Kv3VdBRx~9UpGc zpjsGJQOGd0(U%W7^L>fVgA`+5?dhvgi5=6cm7iK?zOgZ^I#=l6Rl=nNtCbW}%zYFb zvXS5>zhAT9k&syMl1Hzc1NNHnRF7hV7g>LDM0z!F9DJx(K`?}ly6&Gqx(w=1v4VCT zkKW4`0q{Nkf{~H}&(Q(hq?T6nthh%w&8f4JFZ{7 zH(S2%eNhZ&w?HMj4>%4x$}d`HfC{X(2{mC96p_b4t>mB#>R>QQcU<7owV~OghE;dW z&>qA-FqC3eC$=eWzIl&RT6KXMOtcve-uo^LevsFR5;oTwI=R9D7Jf8Wwdl-kXw&!g zai3~42(@@ieier?o~p=4OK>vpOi>gTiW|ya7V=vEY(i4Qeb+GNzd^rYyat0HQ#3Wa}zjltf zlcaYtR1*D>M-^NaRSS#05~sNF*}-23CO6-ClMU}?+R$;>h$*MI7SqyVWrb^S;9lU8 z02z-e;f;FvXY+SDCv1wB;?xv?MmS~B?V(bOKZRwj?DWJA#%3l;17>9KKSz|?Kj{@7s@11@i`{ zI0#c1sDrf9i^6%iYeKFLN!6KJd|2D;Gn52?S#(|ZXE0}T_iQscG1-t7>*UdL@BueW_jtnUkF1C0AjBrZP#Hgk6(KQ9e zw-TH#?2PYz{Ztu42fn>k5#TlYc>-$X#9k5M@QCAfDG>3J(JN{{73DM^dyK#})9H&; z!gQ0a@ju+iR9-2tjc z!gw*X+*PT8uob-f^rPu7#^fOAoG)IaizTX(@mOv2nyXbtmECpJojt}o_^Yh~zB zAM+$gdk7ix=B#K4Nl)9VcD^*$DMyFEyAHRhyg7xtKLuqr_W0;M9KQKpPa;WBCUmT-+s z(hfo<{72axKUj+=iwVwrBZJL%=QA z>#~D+I;bGg%ttP^sUE1?d2MP_zf)>zW53QkE7^{)-=pQ^k2-Jelz_p!&Ajg$>H^fStpZd-_$7-w~-d3ub`DLnAGrgTui% zOA8|1B%{%=S!`%Cm!oBHPB_rNZWFMELzwZwHKb_Nyn9sdq#SY;olG0MOj93;vD?)hg_GG@JCB@7dGAxua}DR=I7I4 z9gUHq_?q^^GH=S)^MRhqhB5wCttKOi3mXNlP4nPUovYFxexJ$4&RqSNya@6)jq)eM z-q^9n99K~!3RLekiH?T^^=fe?`W@H1kKOX8!XM51QK$L^DJ6_^!__8cz**qp7r}zXn})jZAmBf_|P%8#jeq+6PBxXC$COu9Vu|=_{@$L(-4L6DNZJ3tj59}+Y^N`Wb{Ptg^9 z@Sd|r(beB(sGt1TnQBiwuu>3!_39y;KDKLb$EvKs-xg0Omx^T!ZK1YJJ2}L4GT{Sk zV;n|{DO}hwr;key*zxEbHzF*uzN+$@q*jl!QVxxLmMHlJ{Kp5CJPB^*E&uQHoYGoj zx7WV)t<@`gnqLL-JK{K9%c!S~t~vE$I`^p>(vFR=IBr#F)mjgu%d`DZe3JMSaTOeP z#@`@h?ApOp@+;CsAk4w*b>lZv{btZGjdwV(ky;+(Bm4Wi#-&0>gBsVsVe+Be@q!0? zVCepi@YFNbi+hV~)v_t5pU}=*fH3#$R-;UMf$`vM#%7kd`>9mTtWci8V}>kibp zLy$9Ece^MstB&Gf#7g=unj+l~3>Hc&q%g4?qNW${i)Qf$xtZU~(bN>+`0CgMU*?YY zNtzu4v2(`F#Qzt|1`tR7N6H4+mX746_$Y=bhrKwL*7pyofZzW_slfU#FcLr#m6-bz zx)cr}K!KRw+MzMOwNpv1<7$C+>`zV_i*0*vhZ;%6OeGM_0z!8#Wk(TzI;v}+N9Ih2P5*pBq6eSI1IiYulx5N6eE zW5){+Tgyq_hhU}}4+{_)X{w5qUgs}+bBH)HaGaA+K9)`P-{ROBuxVh9un%T5?qi;T z8t#m{f(i0?Sn%T3#}2;(xK-SKP?^B7Y`eRs#y=&W|W=z?xU@8~U%9DDOLc-G*nnq{q z6hj}UYupn`^F+^&mfjSyd2CIdhCKW#aTb&}gnnclnB@`lORw_FvNrRyUrYMx!hN@8 zs1k3#X1NbgS%Ug)QaK%t(O5hmes6IDsDECTUEw`F7C3OGG~hGP&gh@FGhE)Ut$B}c zAuCSKaY%IOip);LN4ba93gYUydu?_~_#-qdnBe1qmV8IE0WAa47)dl;g@qV%}!}Q(4*NMvD&tK$Mo~N*%3~ zB1u4Mwl;2lQ*yKCQ+V9_*3t29V8FYUa8%92bUp+ zpnkWw?-Z;4?F4b+8K4P^tpSW+2!O=;mFl*YG@@mfkI=$@NmJf(4@CuI-xpruPRs(* zA(DZ*4m#n!y^JWUzleZ7I&7vY84(P$KM54wni`TZ1o|C*;#w$VUbvX)`!_Os7GMG^ za>k!3@*>{^YlfVd%E~%H^VQ~${<^sE_T{n-E znB9kAH4M95>VW+#aAax_Ax*~6@uI@)Po&zwu5;t2$XBCz_+ZhPV{s+o9$SZar$=S& zIB-Ag;KeC3<10`JY_0iD)+2Gp&d29a$0u?Y!U{XfsM+yx`7a8%=v({#C%qmTizEOf6;5!LL!syc4lp4t0tNkI7rACHj zsv(e8Xyn@&ryskj4~FhMq_jy=gRLwJ5AQ(WLp?d;8a+I3W2bzNc|RLVLYwaJp)-h7 zm7nxQg?o=}F>fJZZqnb)Re)rPem|Efp>p|=8jTMFCbi#@;1$BnaNG&YC#8PCO+eRe zj@{I@QXq9)ItIa}YW)khxOkjEi4`}S!J8CB{>JdWsH1Y# z{K|?Y4X0w@ds~aNxcP&>K3t+ZWW7tqV@E#LBkE9jn#1(h&V3zt!NHa=;kE^4EHBx8 zcdz;K(*|d$^1gM#Ho*GD2?ALJd>ejKq_JkG@ySrWg%e=^FXzj3#4E%Z+QO`)5uGoZ z!kjCO3y~fXQgS-%%(r_Kgv$kQf!)udZVLZSAOI2y_SgRtnZfszVu*%{6T;EU?}eD% zTV0{*QVd4-_T46N!Cgmu=}JXumM+y81oSOx7b37DRJ~6f9TXyO}9*As6HnaHU_Z<_<2AM`)~y-3T!9+rLub;03y~ zo&y$1?x>Q)Xu#(lz4qX%3+JTF|APoYw9 Date: Tue, 3 Nov 2026 12:35:39 +0100 Subject: [PATCH 22/55] Publish assessment mapping with query bundle --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f4a2946..e316f98 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,6 +64,7 @@ jobs: run: | mkdir pages cp Queries.json pages/Queries.json + cp docs/security-assessment-mapping.json pages/security-assessment-mapping.json - name: Upload GitHub Pages artifact uses: actions/upload-pages-artifact@v4 From 77aceaa7468528cd7a51f6a1084a16d153466090 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 14:21:33 +0100 Subject: [PATCH 23/55] Remove MDI "unmonitored" mappings "Unmonitored" MDI queries are excluded from statistics anyway. --- docs/security-assessment-mapping.json | 57 --------------------------- 1 file changed, 57 deletions(-) diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index cec9549..0f644e8 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -949,63 +949,6 @@ } ] }, - { - "bloodhound_query": { - "guid": "3f0fa2f3-fbdf-42c0-9e7d-97e689009161", - "name": "Collection health of DC Registry Data" - }, - "maps_to": [ - { - "source": "", - "controls": [ - { - "mapping_scope": "partial", - "mapping_scope_detail": "MDI checks whether its sensor is installed on the server. This mapping checks whether the server is represented in BloodHound with the required supported data, which indicates BloodHound coverage. It does not verify MDI sensor deployment.", - "id": "Unmonitored domain controllers", - "name": "Unmonitored domain controllers" - } - ] - } - ] - }, - { - "bloodhound_query": { - "guid": "c8dd3479-8063-450a-9456-557bc5f39e10", - "name": "Collection health of CA Registry Data" - }, - "maps_to": [ - { - "source": "", - "controls": [ - { - "mapping_scope": "partial", - "mapping_scope_detail": "MDI checks whether its sensor is installed on the server. This mapping checks whether the server is represented in BloodHound with the required supported data, which indicates BloodHound coverage. It does not verify MDI sensor deployment.", - "id": "Unmonitored ADCS servers", - "name": "Unmonitored ADCS servers" - } - ] - } - ] - }, - { - "bloodhound_query": { - "guid": "c8dd3479-8063-450a-9456-557bc5f39e10", - "name": "Collection health of DC Registry Data" - }, - "maps_to": [ - { - "source": "", - "controls": [ - { - "mapping_scope": "partial", - "mapping_scope_detail": "MDI checks whether its sensor is installed on the server. This mapping checks whether the server is represented in BloodHound with the required supported data, which indicates BloodHound coverage. It does not verify MDI sensor deployment.", - "id": "Unmonitored ADFS servers", - "name": "Unmonitored ADFS servers" - } - ] - } - ] - }, { "bloodhound_query": { "guid": "421921fa-bc0f-4659-9680-b7481adcb132", From 1f0c202df31031dce4b53ad6a2484ab55e027954 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 14:27:09 +0100 Subject: [PATCH 24/55] Update Computer owners who can obtain LAPS passwords.yml --- queries/Computer owners who can obtain LAPS passwords.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/queries/Computer owners who can obtain LAPS passwords.yml b/queries/Computer owners who can obtain LAPS passwords.yml index a82579a..3a7a83f 100644 --- a/queries/Computer owners who can obtain LAPS passwords.yml +++ b/queries/Computer owners who can obtain LAPS passwords.yml @@ -3,11 +3,15 @@ guid: 92aa81d6-b08e-4abb-ae39-ecbe5735a74c prebuilt: false platforms: Active Directory category: Dangerous Privileges -description: Creators of computer objects get abusable rights on the computer object. If the owner is not explicitly granted ReadLAPSPassword they can still compromise the computer with the abusable owner rights. +description: Owners of enabled LAPS-managed computers whose ownership rights allow reading LAPS passwords or modifying the DACL to grant that access. query: |- - MATCH p=(c:Computer)<-[:Owns|OwnsLimitedRights]-(n:User) + MATCH p=(c:Computer)<-[r:Owns|OwnsLimitedRights]-(n:User) WHERE c.haslaps = true AND c.enabled = true + AND ( + type(r) = 'Owns' + OR ANY(privilege IN r.privileges WHERE privilege IN ['ReadLAPSPassword', 'AllExtendedRights', 'GenericAll', 'WriteDacl']) + ) RETURN p revision: 2 resources: From 9fa528494a65d9329a60acd9987b3d21ff0d14c0 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 14:29:36 +0100 Subject: [PATCH 25/55] Fix WriteDacl edge name in Tier Zero group query --- ...Non-Tier Zero accounts with WriteDACL on Tier Zero group.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/queries/Non-Tier Zero accounts with WriteDACL on Tier Zero group.yml b/queries/Non-Tier Zero accounts with WriteDACL on Tier Zero group.yml index bb28aa9..d07a4cf 100644 --- a/queries/Non-Tier Zero accounts with WriteDACL on Tier Zero group.yml +++ b/queries/Non-Tier Zero accounts with WriteDACL on Tier Zero group.yml @@ -5,7 +5,7 @@ platforms: Active Directory category: Dangerous Privileges description: query: |- - MATCH p=(n:Base)-[:WriteDACL]->(g:Group) + MATCH p=(n:Base)-[:WriteDacl]->(g:Group) WHERE NOT (n:Tag_Tier_Zero) AND (g:Tag_Tier_Zero) RETURN p From fbb7d7d6b4829689eef6a56d9bf9727ac0d369bb Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 14:29:36 +0100 Subject: [PATCH 26/55] Include direct and group-delegated service principal roles --- ...ll privileged Azure Service Principals.yml | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/queries/All privileged Azure Service Principals.yml b/queries/All privileged Azure Service Principals.yml index 738dc39..d38740f 100644 --- a/queries/All privileged Azure Service Principals.yml +++ b/queries/All privileged Azure Service Principals.yml @@ -6,25 +6,10 @@ platforms: category: Azure Hygiene description: Return all privileged Azure Service Principals. query: |- - MATCH p=(n:AZServicePrincipal)-[:AZMemberOf*1..]->(:AZGroup)-[:AZHasRole]->(r:AZRole) - WHERE toUpper(r.roledefinitionid) IN [ - '62E90394-69F5-4237-9190-012177145E10', // Global Administrator - 'FE930BE7-5E62-47DB-91AF-98C3A49A38B1', // User Administrator - '158C047A-C907-4556-B7EF-446551A6B5F7', // Cloud Application Administrator - '0526716B-113D-4C15-B2C8-68E3C22B9F80', // Authentication Policy Administrator - '29232CDF-9323-42FD-ADE2-1D097AF3E4DE', // Exchange Administrator - '729827E3-9C14-49F7-BB1B-9608F156BBB8', // Helpdesk Administrator - '7BE44C8A-ADAF-4E2A-84D6-AB2649E08A13', // Privileged Authentication Administrator - '8329153B-31D0-4727-B945-745EB3BC5F31', // Domain Name Administrator - '8AC3FC64-6ECA-42EA-9E69-59F4C7B60EB2', // Hybrid Identity Administrator - 'BE2F45A1-457D-42AF-A067-6EC1FA63BC45', // External Identity Provider Administrator - 'E8611AB8-C189-46E8-94E1-60213AB1F814', // Privileged Role Administrator - 'E00E864A-17C5-4A4B-9C06-F5B95A8D5BD8', // Partner Tier2 Support - '9B895D92-2CD3-44C7-9D02-A6AC2D5EA5C3', // Application Administrator - 'D29B2B05-8046-44BA-8758-1E26182FCF32' // Directory Synchronization Accounts - ] - RETURN p - LIMIT 1000 + MATCH (n:AZServicePrincipal) + OPTIONAL MATCH p1=(n)-[:AZHasRole]->(:AZRole) + OPTIONAL MATCH p2=(n)-[:AZMemberOf*1..]->(:AZGroup)-[:AZHasRole]->(:AZRole) + RETURN p1, p2 revision: 2 resources: acknowledgements: Daniel Scheidt, @theluemmel From 4b89fcf5c668c129b4d9ce7e61113fc459775b78 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 14:29:37 +0100 Subject: [PATCH 27/55] Fix Guest non-default primary group detection --- ...with non-default Primary Group membership.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/queries/Users with non-default Primary Group membership.yml b/queries/Users with non-default Primary Group membership.yml index bdb0122..6e6bde7 100644 --- a/queries/Users with non-default Primary Group membership.yml +++ b/queries/Users with non-default Primary Group membership.yml @@ -6,22 +6,20 @@ category: Active Directory Hygiene description: query: |- MATCH p=(n:User)-[r:MemberOf]->(g:Group) - WHERE - ( + WHERE r.isprimarygroup = true + AND ( ( - r.isprimarygroup = false - AND n.objectid ENDS WITH "-501" // User: Guest - AND g.objectid ENDS WITH "-514" // Group: Domain Guests + n.objectid ENDS WITH "-501" // User: Guest + AND NOT g.objectid ENDS WITH "-514" // Group: Domain Guests ) OR ( - r.isprimarygroup = true + NOT n.objectid ENDS WITH "-501" // Other users AND NOT g.objectid ENDS WITH "-513" // Group: Domain Users - AND NOT n.objectid ENDS WITH "-501" // User: Guest ) - AND (n.gmsa IS NULL OR n.gmsa = false) // Not gMSA, as it has primaryGroup to Domain Computers - AND (n.msa IS NULL OR n.msa = false) // Not MSA, as it has primaryGroup to Domain Computers ) + AND (n.gmsa IS NULL OR n.gmsa = false) + AND (n.msa IS NULL OR n.msa = false) RETURN p LIMIT 1000 revision: 2 From 5493fe7723f30c556b13bda5779cbc4f08e3ffe1 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 14:39:24 +0100 Subject: [PATCH 28/55] Create run_tenant_queries.py --- utilities/python/run_tenant_queries.py | 176 +++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 utilities/python/run_tenant_queries.py diff --git a/utilities/python/run_tenant_queries.py b/utilities/python/run_tenant_queries.py new file mode 100644 index 0000000..f1e160f --- /dev/null +++ b/utilities/python/run_tenant_queries.py @@ -0,0 +1,176 @@ +"""Run every query in the library against a BloodHound tenant, one at a time. + +Credentials are read from the repository's .env file. The file may use either +KEY=value entries or BloodHound's defautlt output format: ``Key: value`` / ``ID: value`` + +Run with: +.\.venv\Scripts\python.exe utilities\python\run_tenant_queries.py --tenant https://localhost:8080 --show-http-200 +""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import hmac +import json +import os +from pathlib import Path +import sys +from datetime import datetime +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + +import yaml + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +ENDPOINT = "/api/v2/graphs/cypher" + + +def load_env(path: Path) -> dict[str, str]: + values: dict[str, str] = {} + if not path.is_file(): + return values + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + separators = [position for position in (line.find("="), line.find(":")) if position >= 0] + if not separators: + continue + position = min(separators) + name, value = line[:position], line[position + 1:] + values[name.strip().lower()] = value.strip().strip("\"'") + return values + + +def credentials() -> tuple[str, str]: + env = load_env(REPOSITORY_ROOT / ".env") + key = os.environ.get("BH_API_KEY") or env.get("key") or env.get("bh_api_key") + token_id = os.environ.get("BH_API_ID") or env.get("id") or env.get("bh_api_id") + if not key or not token_id: + raise ValueError(".env must contain Key: and ID: (or set BH_API_KEY and BH_API_ID).") + return token_id, key + + +def signed_request(url: str, token_id: str, token_key: str, body: bytes, timeout: int): + request_date = datetime.now().astimezone().isoformat("T") + digester = hmac.new(token_key.encode(), ("POST" + ENDPOINT).encode(), hashlib.sha256) + digester = hmac.new(digester.digest(), request_date[:13].encode(), hashlib.sha256) + digester = hmac.new(digester.digest(), body, hashlib.sha256) + request = Request( + url.rstrip("/") + ENDPOINT, + data=body, + method="POST", + headers={ + "Authorization": f"bhesignature {token_id}", + "RequestDate": request_date, + "Signature": base64.b64encode(digester.digest()).decode(), + "Content-Type": "application/json", + "User-Agent": "BloodHoundQueryLibrary tenant query runner", + }, + ) + try: + with urlopen(request, timeout=timeout) as response: + return response.status, response.read().decode("utf-8", errors="replace") + except HTTPError as error: + return error.code, error.read().decode("utf-8", errors="replace") + except URLError as error: + raise RuntimeError(f"Request failed: {error.reason}") from error + + +def is_api_error(status: int, payload: object) -> bool: + if not 200 <= status < 300: + return True + if isinstance(payload, dict): + if payload.get("errors"): + return True + status_value = str(payload.get("status", "")).lower() + if status_value in {"error", "failed"}: + return True + return False + + +def result_count(payload: object) -> int | None: + if not isinstance(payload, dict): + return None + data = payload.get("data") + if isinstance(data, dict): + graph_keys = ("nodes", "edges", "literals") + if any(key in data for key in graph_keys): + return sum( + len(data[key]) + for key in graph_keys + if isinstance(data.get(key), (dict, list)) + ) + for key in ("nodes", "rows", "results"): + value = data.get(key) + if isinstance(value, (dict, list)): + return len(value) + if isinstance(data, list): + return len(data) + return None + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--tenant", required=True, help="BloodHound tenant base URL, for example https://tenant.example.com") + parser.add_argument("--queries", type=Path, default=REPOSITORY_ROOT / "queries") + parser.add_argument("--timeout", type=int, default=1800, help="Per-query HTTP timeout in seconds (default: 1800).") + parser.add_argument( + "--show-http-200", + action="store_true", + help="Print each successful HTTP 200 response and its result content.", + ) + args = parser.parse_args() + + try: + token_id, token_key = credentials() + except ValueError as error: + parser.error(str(error)) + + files = sorted(args.queries.rglob("*.yml")) + if not files: + print(f"No query YAML files found in {args.queries}", file=sys.stderr) + return 2 + + failures = 0 + empty_results = 0 + print(f"Running {len(files)} queries against {args.tenant} sequentially (timeout {args.timeout}s each).") + for index, path in enumerate(files, 1): + try: + query_doc = yaml.safe_load(path.read_text(encoding="utf-8")) + name = query_doc["name"] + query = query_doc["query"] + body = json.dumps({"query": query, "include_properties": False}).encode("utf-8") + status, response_text = signed_request(args.tenant, token_id, token_key, body, args.timeout) + try: + payload = json.loads(response_text) + except json.JSONDecodeError: + payload = {"response": response_text} + count = result_count(payload) + state = "FAIL" if is_api_error(status, payload) else "PASS" + if state == "FAIL": + failures += 1 + elif count == 0: + empty_results += 1 + state = "INFO" + if status != 200 or args.show_http_200: + print(f"\n[{index}/{len(files)}] {state} {name} ({path.name}) HTTP {status}") + if count is not None: + print(f"Result items: {count}" + (" (no results; this may reflect tenant data or query conditions)" if count == 0 else "")) + print(json.dumps(payload, indent=2, ensure_ascii=False)) + except Exception as error: # Keep processing subsequent queries. + failures += 1 + print(f"\n[{index}/{len(files)}] FAIL {path.name}: {type(error).__name__}: {error}") + + print( + f"\nCompleted: {len(files) - failures} passed/informational, {failures} failed, " + f"{empty_results} with no results." + ) + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 48c010346d04b230c5650393b2837bd625344175 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 14:53:40 +0100 Subject: [PATCH 29/55] Fix MDI source for privileged external account mapping --- docs/security-assessment-mapping.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index 0f644e8..3090842 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -3641,7 +3641,12 @@ "mapping_scope_detail": "Expanded scope to Tier Zero", "id": "5c8bd887-55ac-47d6-9835-6ed5370cda26", "name": "Privileged group contains guest account" - }, + } + ] + }, + { + "source": "MDI", + "controls": [ { "mapping_scope": "superset", "mapping_scope_detail": "Expanded scope to Tier Zero", From a01098054002ef013ae1678becc63b14c9a75910 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 14:54:45 +0100 Subject: [PATCH 30/55] Fix MDI sources for Operator and service account mappings --- docs/security-assessment-mapping.json | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index 3090842..d6c50d4 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -1593,7 +1593,12 @@ "mapping_scope_detail": "", "id": "P-OperatorsEmpty", "name": "[M]Check that the operator groups are empty" - }, + } + ] + }, + { + "source": "MDI", + "controls": [ { "mapping_scope": "exact", "mapping_scope_detail": "", @@ -1923,7 +1928,12 @@ "mapping_scope_detail": "Expanded account scope to include accounts with SPN. Expanded group scope to include Enterprise Admins and Schema Admins", "id": "P-ServiceDomainAdmin", "name": "[M][T]Check if Service Accounts are domain administrators" - }, + } + ] + }, + { + "source": "MDI", + "controls": [ { "mapping_scope": "exact", "mapping_scope_detail": "", From c8a53904089994dd74b415b9846d5937dcd082c0 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 14:56:22 +0100 Subject: [PATCH 31/55] Update run_tenant_queries.py --- utilities/python/run_tenant_queries.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/utilities/python/run_tenant_queries.py b/utilities/python/run_tenant_queries.py index f1e160f..5fc1c82 100644 --- a/utilities/python/run_tenant_queries.py +++ b/utilities/python/run_tenant_queries.py @@ -1,4 +1,4 @@ -"""Run every query in the library against a BloodHound tenant, one at a time. +r"""Run every query in the library against a BloodHound tenant, one at a time. Credentials are read from the repository's .env file. The file may use either KEY=value entries or BloodHound's defautlt output format: ``Key: value`` / ``ID: value`` @@ -18,6 +18,7 @@ from pathlib import Path import sys from datetime import datetime +from collections import Counter from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen @@ -137,6 +138,7 @@ def main() -> int: failures = 0 empty_results = 0 + http_status_counts: Counter[int] = Counter() print(f"Running {len(files)} queries against {args.tenant} sequentially (timeout {args.timeout}s each).") for index, path in enumerate(files, 1): try: @@ -145,6 +147,7 @@ def main() -> int: query = query_doc["query"] body = json.dumps({"query": query, "include_properties": False}).encode("utf-8") status, response_text = signed_request(args.tenant, token_id, token_key, body, args.timeout) + http_status_counts[status] += 1 try: payload = json.loads(response_text) except json.JSONDecodeError: @@ -169,6 +172,10 @@ def main() -> int: f"\nCompleted: {len(files) - failures} passed/informational, {failures} failed, " f"{empty_results} with no results." ) + if http_status_counts: + print("HTTP status counts:") + for status, count in sorted(http_status_counts.items()): + print(f" HTTP {status}: {count}") return 1 if failures else 0 From f122cbf809284e217244ee3391821317db381cec Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 15:47:42 +0100 Subject: [PATCH 32/55] Create AD query: Foreign principals in Tier Zero groups --- docs/security-assessment-mapping.json | 8 ++++---- .../Foreign principals in Tier Zero groups.yml | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 queries/Foreign principals in Tier Zero groups.yml diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index d6c50d4..599e9f5 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -3076,16 +3076,16 @@ }, { "bloodhound_query": { - "guid": "95bec736-86ef-4017-8465-9b9b66548b17", - "name": "Foreign principals in Tier Zero / High Value targets" + "guid": "bf8701bb-964f-484e-98e9-6a5dc9a0b35f", + "name": "Foreign principals in Tier Zero groups" }, "maps_to": [ { "source": "PurpleKnight", "controls": [ { - "mapping_scope": "exact", - "mapping_scope_detail": "", + "mapping_scope": "superset", + "mapping_scope_detail": "Expanded exclusion scope to Tier Zero", "id": "3bcaff82-ae70-4646-bc66-f7997be54e5e", "name": "Foreign Security Principals in Privileged Group" } diff --git a/queries/Foreign principals in Tier Zero groups.yml b/queries/Foreign principals in Tier Zero groups.yml new file mode 100644 index 0000000..a588432 --- /dev/null +++ b/queries/Foreign principals in Tier Zero groups.yml @@ -0,0 +1,16 @@ +name: Foreign principals in Tier Zero groups +guid: bf8701bb-964f-484e-98e9-6a5dc9a0b35f +prebuilt: false +platforms: Active Directory +category: Dangerous Privileges +description: Identifies foreign domain principals that are direct members of Tier Zero groups. +query: |- + MATCH p=(s:Base)-[:MemberOf]->(t:Group) + WHERE s.domainsid <> t.domainsid + AND (t:Tag_Tier_Zero) + RETURN p + LIMIT 1000 +revision: 1 +resources: +acknowledgements: Martin Sohn Christensen, @martinsohndk + From 1ae2c27d5f285ee257584320b0abdd139ba3b5c8 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 15:47:50 +0100 Subject: [PATCH 33/55] Create: Computers with passwords older than the default maximum password age - MSSQL Failover Cluster --- docs/security-assessment-mapping.json | 4 ++-- ... password age - MSSQL Failover Cluster.yml | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 queries/Computers with passwords older than the default maximum password age - MSSQL Failover Cluster.yml diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index 599e9f5..33d90f7 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -16,8 +16,8 @@ "mappings": [ { "bloodhound_query": { - "guid": "d263e621-7f1b-4efb-ad25-098fc7d4fb72", - "name": "Enabled computers inactive for 180 days - MSSQL Failover Cluster" + "guid": "8fbb372e-f3fd-4fc8-a362-06d6315812c9", + "name": "MSSQL Failover Cluster accounts with passwords older than 60 days" }, "maps_to": [ { diff --git a/queries/Computers with passwords older than the default maximum password age - MSSQL Failover Cluster.yml b/queries/Computers with passwords older than the default maximum password age - MSSQL Failover Cluster.yml new file mode 100644 index 0000000..3699955 --- /dev/null +++ b/queries/Computers with passwords older than the default maximum password age - MSSQL Failover Cluster.yml @@ -0,0 +1,24 @@ +name: Computers with passwords older than the default maximum password age - MSSQL Failover Cluster +guid: 8fbb372e-f3fd-4fc8-a362-06d6315812c9 +prebuilt: false +platforms: Active Directory +category: Active Directory Hygiene +description: Identifies enabled MSSQL failover cluster computer accounts whose password has not changed in the last 60 days and have logged on recently. +query: |- + WITH 60 AS password_age_days + MATCH (n:Computer) + WHERE n.enabled = true + AND n.whencreated < (datetime().epochseconds - (password_age_days * 86400)) + AND n.lastlogontimestamp > (datetime().epochseconds - (60 * 86400)) + AND n.lastlogon > (datetime().epochseconds - (60 * 86400)) + AND n.pwdlastset < (datetime().epochseconds - (password_age_days * 86400)) + AND ANY(spn IN n.serviceprincipalnames WHERE + toLower(spn) CONTAINS 'mssqlservercluster' OR + toLower(spn) CONTAINS 'mssqlserverclustermgmtapi' OR + toLower(spn) CONTAINS 'msclustervirtualserver') + RETURN n + LIMIT 1000 +revision: 1 +resources: +- https://learn.microsoft.com/en-us/troubleshoot/windows-server/high-availability/troubleshoot-issues-accounts-used-failover-clusters#troubleshoot-password-issues-with-the-cluster-name-account +acknowledgements: Martin Sohn Christensen, @martinsohndk From 3d5d23ea0b3ea4c84dae9e224b066738b202b67a Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 16:03:05 +0100 Subject: [PATCH 34/55] Add more functional levels to: Domains with functional level not the latest version --- ...Domains with functional level not the latest version.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/queries/Domains with functional level not the latest version.yml b/queries/Domains with functional level not the latest version.yml index 8012de2..877d9ed 100644 --- a/queries/Domains with functional level not the latest version.yml +++ b/queries/Domains with functional level not the latest version.yml @@ -6,9 +6,9 @@ category: Active Directory Hygiene description: query: |- MATCH (n:Domain) - WHERE toString(n.functionallevel) IN ['2008','2003','2003 Interim','2000 Mixed/Native'] + WHERE toString(n.functionallevel) IN ['2016', '2012 R2', '2012', '2008','2003','2003 Interim','2000 Mixed/Native'] RETURN n -revision: 1 -resources: +revision: 2 +resources: https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/active-directory-functional-levels acknowledgements: Martin Sohn Christensen, @martinsohndk From 7ecc61fbf2c8ec22d2c19cddf50322908022d109 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 16:05:15 +0100 Subject: [PATCH 35/55] Add W10 to: Computers with unsupported operating systems --- docs/security-assessment-mapping.json | 4 ++-- queries/Computers with unsupported operating systems.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index 33d90f7..864fa2a 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -722,8 +722,8 @@ "source": "PingCastle", "controls": [ { - "mapping_scope": "exact", - "mapping_scope_detail": "", + "mapping_scope": "partial", + "mapping_scope_detail": "Flags Windows 10 without verifying ESU enrollment or LTSC support status. Does not detect unsupported Windows 11 releases.", "id": "S-OS-W10", "name": "[M]Obsolete OS (Windows 10 or Windows 11)" }, diff --git a/queries/Computers with unsupported operating systems.yml b/queries/Computers with unsupported operating systems.yml index 48c606e..22f07fc 100644 --- a/queries/Computers with unsupported operating systems.yml +++ b/queries/Computers with unsupported operating systems.yml @@ -3,13 +3,13 @@ guid: d06d3b14-0318-4fa9-9639-4b79ccaf3c2c prebuilt: true platforms: Active Directory category: Active Directory Hygiene -description: +description: Identifies potentially unsupported Windows versions, including Windows 10. Windows 10 results do not verify ESU enrollment or LTSC support status. Unsupported Windows 11 releases are not detected. query: |- MATCH (c:Computer) - WHERE c.operatingsystem =~ '(?i).*Windows.* (2000|2003|2008|2012|xp|vista|7|8|me|nt).*' + WHERE c.operatingsystem =~ '(?i).*Windows.* (2000|2003|2008|2012|xp|vista|7|8|10|me|nt).*' RETURN c LIMIT 100 -revision: 1 +revision: 2 resources: acknowledgements: From 4be29a6dcc465676cae5da17734b02b9b773a97f Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 16:15:26 +0100 Subject: [PATCH 36/55] Remove duplicate mapping for PingCastle: A-CertTempCustomSubject --- docs/security-assessment-mapping.json | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index 864fa2a..12c4a6b 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -2367,25 +2367,6 @@ } ] }, - { - "bloodhound_query": { - "guid": "96e70597-2d74-4503-a624-f1e30b642894", - "name": "Enrollment rights on certificate templates published to Enterprise CA with User Specified SAN enabled" - }, - "maps_to": [ - { - "source": "PingCastle", - "controls": [ - { - "mapping_scope": "exact", - "mapping_scope_detail": "Checks ESC1 scenario which is also covered by the query 49db8edc-8421-438f-b97b-23c042959bef", - "id": "A-CertTempCustomSubject", - "name": "[T]Check if authentication certificate templates allow users to control the subject" - } - ] - } - ] - }, { "bloodhound_query": { "guid": "60881923-296c-4702-adf7-a4f059dc9bb8", From b2000be84ff00fdc94fa8b4a88e12fe3df89c504 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 16:22:04 +0100 Subject: [PATCH 37/55] Map PurpleKnight hybrid privileges to dual Tier Zero query --- docs/security-assessment-mapping.json | 41 +++++++-------------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index 12c4a6b..cfb6be5 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -277,6 +277,17 @@ "name": "Microsoft Entra ID privileged user accounts that are also privileged in Active Directory" } ] + }, + { + "source": "PurpleKnight", + "controls": [ + { + "mapping_scope": "superset", + "mapping_scope_detail": "Expanded scope to Tier Zero", + "id": "c117b6a5-00c8-4e77-93d2-e291e36b462a", + "name": "Entra ID privileged users that are also privileged in AD" + } + ] } ] }, @@ -3196,36 +3207,6 @@ "name": "AD privileged users that are synced to Entra ID" } ] - }, - { - "source": "PurpleKnight", - "controls": [ - { - "mapping_scope": "combination", - "mapping_scope_detail": "Finds the direction AD -> Entra only.", - "id": "c117b6a5-00c8-4e77-93d2-e291e36b462a", - "name": "Entra ID privileged users that are also privileged in AD" - } - ] - } - ] - }, - { - "bloodhound_query": { - "guid": "0d65c62e-bd77-4027-b967-fb36690739c9", - "name": "Tier Zero Entra ID principals synchronized with AD" - }, - "maps_to": [ - { - "source": "PurpleKnight", - "controls": [ - { - "mapping_scope": "combination", - "mapping_scope_detail": "Finds the direction Entra -> AD only.", - "id": "c117b6a5-00c8-4e77-93d2-e291e36b462a", - "name": "Entra ID privileged users that are also privileged in AD" - } - ] } ] }, From 4cee49a14f78b0946ea726cfb21450a6e264dcc6 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 16:29:27 +0100 Subject: [PATCH 38/55] Clarify combined LDAP relay assessment mapping scopes --- docs/security-assessment-mapping.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index cfb6be5..70b91ce 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -2525,14 +2525,14 @@ "source": "PingCastle", "controls": [ { - "mapping_scope": "exact", - "mapping_scope_detail": "", + "mapping_scope": "superset", + "mapping_scope_detail": "Query returns DCs where both LDAP signing is not required or LDAPS channel binding is not enforced.", "id": "A-DCLdapSign", "name": "[T]Check if signing is really required for LDAP" }, { - "mapping_scope": "exact", - "mapping_scope_detail": "", + "mapping_scope": "superset", + "mapping_scope_detail": "Query returns DCs where both LDAP signing is not required or LDAPS channel binding is not enforced.", "id": "A-DCLdapsChannelBinding", "name": "[T]Check if the Channel Binding is enabled for LDAPS" } @@ -2542,8 +2542,8 @@ "source": "MDI", "controls": [ { - "mapping_scope": "exact", - "mapping_scope_detail": "", + "mapping_scope": "superset", + "mapping_scope_detail": "Query returns DCs where both LDAP signing is not required or LDAPS channel binding is not enforced.", "id": "Resolve unsecure domain configurations: Enforce LDAP Signing policy to \"Require signing\"", "name": "Resolve unsecure domain configurations: Enforce LDAP Signing policy to \"Require signing\"" } @@ -2553,8 +2553,8 @@ "source": "PurpleKnight", "controls": [ { - "mapping_scope": "exact", - "mapping_scope_detail": "", + "mapping_scope": "superset", + "mapping_scope_detail": "Query returns DCs where both LDAP signing is not required or LDAPS channel binding is not enforced.", "id": "4fe825ed-07fb-4b06-913a-be5c9542ca54", "name": "LDAP signing is not required on Domain Controllers" } From c75c39b230474c1bffbe61f618a128750c1c2bf1 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 16:40:05 +0100 Subject: [PATCH 39/55] Add Administrators to: Kerberos-enabled service account member of built-in Admins groups --- ...nabled service account member of built-in Admins groups.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/queries/Kerberos-enabled service account member of built-in Admins groups.yml b/queries/Kerberos-enabled service account member of built-in Admins groups.yml index 4961f49..414e0b1 100644 --- a/queries/Kerberos-enabled service account member of built-in Admins groups.yml +++ b/queries/Kerberos-enabled service account member of built-in Admins groups.yml @@ -10,10 +10,11 @@ query: |- g.objectid ENDS WITH '-512' // Domain Admins OR g.objectid ENDS WITH '-519' // Enterprise Admins OR g.objectid ENDS WITH '-518' // Schema Admins + OR g.objectid ENDS WITH 'S-1-5-32-544' // Administrators ) AND n.hasspn = true RETURN p -revision: 1 +revision: 2 resources: acknowledgements: Martin Sohn Christensen, @martinsohndk From 86150c3e58f413245d518e1e3eaa5cfa503c1d9e Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 16:41:41 +0100 Subject: [PATCH 40/55] Consolidate duplicate Tier Zero ownership mapping --- docs/security-assessment-mapping.json | 47 ++++++++++----------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index 70b91ce..e21efb5 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -1339,6 +1339,23 @@ "name": "[M]At least one domain controller is not owned correctly" } ] + }, + { + "source": "PurpleKnight", + "controls": [ + { + "mapping_scope": "superset", + "mapping_scope_detail": "Remove false positives by reducing source scope to Non-Tier Zero. Expanded target scope to Tier Zero", + "id": "d2df85d9-abbc-4585-be11-123a6d90a871", + "name": "Domain Controller owner is not an administrator" + }, + { + "mapping_scope": "superset", + "mapping_scope_detail": "Remove false positives by reducing source scope to Non-Tier Zero. Expanded target scope to Tier Zero", + "id": "40499bf5-9087-4d55-9db3-2cb641a47ac8", + "name": "Privileged objects with unprivileged owners" + } + ] } ] }, @@ -3161,36 +3178,6 @@ } ] }, - { - "bloodhound_query": { - "guid": "99d29ded-223a-442b-a0e0-f8b5694c6441", - "name": "Tier Zero principals not owned by Tier Zero" - }, - "maps_to": [ - { - "source": "PurpleKnight", - "controls": [ - { - "mapping_scope": "superset", - "mapping_scope_detail": "Remove false positives by reducing source scope to Non-Tier Zero. Expanded target scope to Tier Zero", - "id": "d2df85d9-abbc-4585-be11-123a6d90a871", - "name": "Domain Controller owner is not an administrator" - } - ] - }, - { - "source": "PurpleKnight", - "controls": [ - { - "mapping_scope": "superset", - "mapping_scope_detail": "Remove false positives by reducing source scope to Non-Tier Zero. Expanded target scope to Tier Zero", - "id": "40499bf5-9087-4d55-9db3-2cb641a47ac8", - "name": "Privileged objects with unprivileged owners" - } - ] - } - ] - }, { "bloodhound_query": { "guid": "a8b6ec67-21aa-4dd2-8906-47bb81bf5262", From 97e52a7ca49027202b70c59149272bb6216775cd Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 17:11:52 +0100 Subject: [PATCH 41/55] Add AES configuration query and update assessment mappings --- docs/security-assessment-mapping.json | 12 +++++----- ...ervice accounts without AES encryption.yml | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 queries/Kerberos-enabled service accounts without AES encryption.yml diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index e21efb5..e90fb44 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -809,8 +809,8 @@ }, { "bloodhound_query": { - "guid": "cb8cf96e-21c9-422b-9439-390a13446ca6", - "name": "Kerberos-enabled service accounts permitting RC4 encryption" + "guid": "8ff729ab-74e2-44a7-b02b-e9a81137c44b", + "name": "Kerberos-enabled service accounts without AES encryption" }, "maps_to": [ { @@ -818,7 +818,7 @@ "controls": [ { "mapping_scope": "exact", - "mapping_scope_detail": "PC flags service accounts without AES flag. Following the RC4 disablement enforcement of CVE-2026-20833, the Key Distribution Center no longer assumes RC4 support for accounts without a msDS-SupportedEncryptionTypes value, so the BH query instead flags accounts where RC4 is explicitly permitted thus covers the same risk.", + "mapping_scope_detail": "Assumes updated domain controllers with default AES KDC settings and available AES keys; unset and empty encryption configurations use the AES default.", "id": "S-AesNotEnabled", "name": "[T]Check the use of Kerberos on services accounts without AES support" } @@ -829,7 +829,7 @@ "controls": [ { "mapping_scope": "exact", - "mapping_scope_detail": "MDI flags service accounts without AES flag. Following the RC4 disablement enforcement of CVE-2026-20833, the Key Distribution Center no longer assumes RC4 support for accounts without a msDS-SupportedEncryptionTypes value, so the BH query instead flags accounts where RC4 is explicitly permitted thus covers the same risk.", + "mapping_scope_detail": "Assumes updated domain controllers with default AES KDC settings and available AES keys; unset and empty encryption configurations use the AES default.", "id": "Unsecure account attributes: Enable Kerberos AES encryption support", "name": "Unsecure account attributes: Enable Kerberos AES encryption support" } @@ -839,8 +839,8 @@ "source": "PurpleKnight", "controls": [ { - "mapping_scope": "partial", - "mapping_scope_detail": "BH query matches all base nodes with SPNs (users and computers), while PK targets only user accounts. PK flags accounts not supporting AES, while the BH query flags accounts where RC4 is explicitly permitted following the RC4 disablement enforcement of CVE-2026-20833.", + "mapping_scope": "superset", + "mapping_scope_detail": "Assumes updated domain controllers with default AES KDC settings and available AES keys; unset and empty encryption configurations use the AES default. Expanded scope to include computer accounts with SPNs as well as users.", "id": "b608276e-3849-419d-bc34-6e5a362b3e79", "name": "Primary users with SPN not supporting AES encryption on Kerberos" } diff --git a/queries/Kerberos-enabled service accounts without AES encryption.yml b/queries/Kerberos-enabled service accounts without AES encryption.yml new file mode 100644 index 0000000..3aee140 --- /dev/null +++ b/queries/Kerberos-enabled service accounts without AES encryption.yml @@ -0,0 +1,22 @@ +name: Kerberos-enabled service accounts without AES encryption +guid: 8ff729ab-74e2-44a7-b02b-e9a81137c44b +prebuilt: false +platforms: Active Directory +category: Active Directory Hygiene +description: SPN accounts with an explicit supported encryption type configuration that excludes AES128 and AES256 ticket encryption. Unset and empty configurations are excluded because updated domain controllers default to AES under CVE-2026-20833 enforcement. Assumes default KDC settings and available AES keys; does not inspect effective KDC policy or key material. +query: |- + MATCH (n:Base) + WHERE n.hasspn = true + // Unset or zero configurations use the KDC default (AES on updated DCs). + AND SIZE(n.supportedencryptiontypes) > 0 + AND NONE(encryption_type IN n.supportedencryptiontypes WHERE encryption_type IN [ + 'AES128-CTS-HMAC-SHA1-96', + 'AES256-CTS-HMAC-SHA1-96' + ]) + RETURN n + LIMIT 1000 +revision: 1 +resources: +- https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/6cfc7b50-11ed-4b4d-846d-6f08f0812919 +- https://support.microsoft.com/en-us/servicing/os/windows/2025/11/how-to-manage-kerberos-kdc-usage-of-rc4-for-service-account-ticket-issuance-changes-related-to-cve-2 +acknowledgements: Martin Sohn Christensen, @martinsohndk From e041e28523d3fd7599b79b21cf903c166a9fc9b7 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 17:17:50 +0100 Subject: [PATCH 42/55] Update Computers with non-default Primary Group membership.yml --- ...h non-default Primary Group membership.yml | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/queries/Computers with non-default Primary Group membership.yml b/queries/Computers with non-default Primary Group membership.yml index 2950851..edaaa0e 100644 --- a/queries/Computers with non-default Primary Group membership.yml +++ b/queries/Computers with non-default Primary Group membership.yml @@ -5,27 +5,29 @@ platforms: Active Directory category: Active Directory Hygiene description: query: |- - // Domain Controllers - MATCH p1=(n:Computer)-[r:MemberOf]->(g:Group) - WHERE NOT g.objectid ENDS WITH "-516" - AND n.isdc = true - AND r.isprimarygroup = true - - // Domain Computers - OPTIONAL MATCH p2=(n:Computer)-[r:MemberOf]->(g:Group) - WHERE NOT g.objectid ENDS WITH "-515" - AND NOT n.isdc = true - AND NOT n.isreadonlydc = true - AND r.isprimarygroup = true - - // Read-Only Domain Controllers - OPTIONAL MATCH p3=(n:Computer)-[r:MemberOf]->(g:Group) - WHERE NOT g.objectid ENDS WITH "-521" - AND n.isreadonlydc = true - AND r.isprimarygroup = true - - RETURN p1,p2,p3 -revision: 3 + MATCH p=(n:Computer)-[r:MemberOf]->(g:Group) + WHERE r.isprimarygroup = true + AND ( + ( + n.isreadonlydc = true + AND NOT g.objectid ENDS WITH "-521" // Read-Only Domain Controllers + ) + OR + ( + n.isdc = true + AND coalesce(n.isreadonlydc, false) = false + AND NOT g.objectid ENDS WITH "-516" // Domain Controllers + ) + OR + ( + coalesce(n.isdc, false) = false + AND coalesce(n.isreadonlydc, false) = false + AND NOT g.objectid ENDS WITH "-515" // Domain Computers + ) + ) + RETURN p + LIMIT 1000 +revision: 4 resources: - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-ada3/e12954a4-6865-4432-94e6-00c310ca87c0 - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/5dbcf875-e802-4357-a6e2-1bdff19ff9b5 From 1eeb517f5852230246d526614c8732fdab0c387f Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 17:18:38 +0100 Subject: [PATCH 43/55] Update All DNSAdmins.yml --- queries/All DNSAdmins.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/queries/All DNSAdmins.yml b/queries/All DNSAdmins.yml index 243b914..0181c53 100644 --- a/queries/All DNSAdmins.yml +++ b/queries/All DNSAdmins.yml @@ -5,10 +5,10 @@ platforms: Active Directory category: Domain Information description: query: |- - MATCH p=(n:Base)-[:MemberOf]->(g:Group) - WHERE n.name STARTS WITH "DNSADMINS@" + MATCH p=(n:Base)-[:MemberOf*1..]->(g:Group) + WHERE g.name STARTS WITH "DNSADMINS@" RETURN p -revision: 1 +revision: 2 resources: acknowledgements: Martin Sohn Christensen, @martinsohndk From b3740ea2970b31bf810e76269be28a423f3927d7 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 17:22:24 +0100 Subject: [PATCH 44/55] Update Tier Zero accounts not members of Denied RODC Password Replication Group.yml --- ... not members of Denied RODC Password Replication Group.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/queries/Tier Zero accounts not members of Denied RODC Password Replication Group.yml b/queries/Tier Zero accounts not members of Denied RODC Password Replication Group.yml index 1edadba..40a8688 100644 --- a/queries/Tier Zero accounts not members of Denied RODC Password Replication Group.yml +++ b/queries/Tier Zero accounts not members of Denied RODC Password Replication Group.yml @@ -9,7 +9,7 @@ query: |- MATCH (n:Base)-[:MemberOf*1..]->(m:Group) WHERE (n:Tag_Tier_Zero) AND (n:User OR n:Computer) - AND m.objectid ENDS WITH '-519' + AND m.objectid ENDS WITH '-572' // Denied RODC Password Replication Group WITH COLLECT(n.objectid) AS MembersOfDeniedGroup // Get all Tier Zero accounts @@ -20,7 +20,7 @@ query: |- // Filter the members of Denied RODC Password Replication Group AND NOT x.objectid IN MembersOfDeniedGroup RETURN x -revision: 3 +revision: 4 resources: acknowledgements: Martin Sohn Christensen, @martinsohndk From cccf971e54a5f9a9b42b1bf4d730f40cb6eabbfc Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 17:25:47 +0100 Subject: [PATCH 45/55] Update Accounts with SID History to a non-existent domain.yml --- .../Accounts with SID History to a non-existent domain.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/queries/Accounts with SID History to a non-existent domain.yml b/queries/Accounts with SID History to a non-existent domain.yml index 991a358..5bcd78a 100644 --- a/queries/Accounts with SID History to a non-existent domain.yml +++ b/queries/Accounts with SID History to a non-existent domain.yml @@ -8,9 +8,9 @@ query: |- MATCH (d:Domain) WITH collect(d.objectid) AS domainSIDs MATCH p=(n:Base)-[:HasSIDHistory]->(m:Base) - WHERE NOT n.domainsid IN domainSIDs + WHERE NOT m.domainsid IN domainSIDs RETURN p -revision: 1 +revision: 2 resources: acknowledgements: Martin Sohn Christensen, @martinsohndk From 74e9d9932808c2ea975106e3773ee8518d6952f9 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Tue, 3 Nov 2026 17:35:47 +0100 Subject: [PATCH 46/55] Fix syntax: Kerberos-enabled service accounts without AES encryption --- ...rberos-enabled service accounts without AES encryption.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/queries/Kerberos-enabled service accounts without AES encryption.yml b/queries/Kerberos-enabled service accounts without AES encryption.yml index 3aee140..ba4a30a 100644 --- a/queries/Kerberos-enabled service accounts without AES encryption.yml +++ b/queries/Kerberos-enabled service accounts without AES encryption.yml @@ -7,8 +7,8 @@ description: SPN accounts with an explicit supported encryption type configurati query: |- MATCH (n:Base) WHERE n.hasspn = true - // Unset or zero configurations use the KDC default (AES on updated DCs). - AND SIZE(n.supportedencryptiontypes) > 0 + AND ANY(encryption_type IN n.supportedencryptiontypes + WHERE encryption_type <> 'Not defined') AND NONE(encryption_type IN n.supportedencryptiontypes WHERE encryption_type IN [ 'AES128-CTS-HMAC-SHA1-96', 'AES256-CTS-HMAC-SHA1-96' From e2a87cb017b89e22a00e1af90af10e1a16c97b67 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Wed, 4 Nov 2026 12:17:22 +0100 Subject: [PATCH 47/55] Remove redundant query and replace mapping for P-DelegationEveryone "Large default groups with outbound control of OUs" was largely redundant and needed asjustments to comply with the actual P-DelegationEveryone check. Now the mapping is set to "Large default groups with outbound control" which covers more risk. --- docs/security-assessment-mapping.json | 6 +++--- ...lt groups with outbound control of OUs.yml | 20 ------------------- 2 files changed, 3 insertions(+), 23 deletions(-) delete mode 100644 queries/Large default groups with outbound control of OUs.yml diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index e90fb44..7770b04 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -1729,8 +1729,8 @@ }, { "bloodhound_query": { - "guid": "310b3626-f8e6-4ab0-832c-72df6048597f", - "name": "Large default groups with outbound control of OUs" + "guid": "a334f21a-3d7f-448e-b7ea-1465a3127bce", + "name": "Large default groups with outbound control" }, "maps_to": [ { @@ -1738,7 +1738,7 @@ "controls": [ { "mapping_scope": "superset", - "mapping_scope_detail": "Expanded technique scope to include all BloodHound traversable edges", + "mapping_scope_detail": "Expanded technique scope to include all BloodHound traversable edges on all object types", "id": "P-DelegationEveryone", "name": "[M][T]A Delegation is granted to Everyone" } diff --git a/queries/Large default groups with outbound control of OUs.yml b/queries/Large default groups with outbound control of OUs.yml deleted file mode 100644 index 68e69ae..0000000 --- a/queries/Large default groups with outbound control of OUs.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Large default groups with outbound control of OUs -guid: 310b3626-f8e6-4ab0-832c-72df6048597f -prebuilt: false -platforms: Active Directory -category: Dangerous Privileges -description: -query: |- - MATCH p=(n:Group)-[]->(:OU) - WHERE n.objectid ENDS WITH "-513" // DOMAIN USERS - OR n.objectid ENDS WITH "-515" // DOMAIN COMPUTERS - OR n.objectid ENDS WITH "-S-1-5-11" // AUTHENTICATED USERS - OR n.objectid ENDS WITH "-S-1-1-0" // EVERYONE - OR n.objectid ENDS WITH "S-1-5-32-545" // USERS - OR n.objectid ENDS WITH "S-1-5-32-546" // GUESTS - OR n.objectid ENDS WITH "S-1-5-7" // ANONYMOUS - RETURN p -revision: 1 -resources: -acknowledgements: Martin Sohn Christensen, @martinsohndk - From aa9ddc28a0ce4d8991272874e650def226940321 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Wed, 4 Nov 2026 12:22:25 +0100 Subject: [PATCH 48/55] All traversable edges except memberships: Large default groups with outbound control --- ...e default groups with outbound control.yml | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/queries/Large default groups with outbound control.yml b/queries/Large default groups with outbound control.yml index cfcb4ac..e7d4578 100644 --- a/queries/Large default groups with outbound control.yml +++ b/queries/Large default groups with outbound control.yml @@ -5,16 +5,19 @@ platforms: Active Directory category: Dangerous Privileges description: query: |- - MATCH p=(n:Group)-[:Owns|GenericAll|GenericWrite|WriteOwner|WriteDacl|ForceChangePassword|AllExtendedRights|AddMember|AllowedToDelegate|AllowedToAct|AdminTo|CanPSRemote|CanRDP|ExecuteDCOM|HasSIDHistory|AddSelf|DCSync|ReadLAPSPassword|ReadGMSAPassword|DumpSMSAPassword|SQLAdmin|AddAllowedToAct|WriteSPN|AddKeyCredentialLink|SyncLAPSPassword|WriteAccountRestrictions|GoldenCert|ADCSESC1|ADCSESC3|ADCSESC4|ADCSESC5|ADCSESC6a|ADCSESC6b|ADCSESC7|ADCSESC9a|ADCSESC9b|ADCSESC10a|ADCSESC10b|ADCSESC13]->(:Base) - WHERE n.objectid ENDS WITH "-513" // DOMAIN USERS - OR n.objectid ENDS WITH "-515" // DOMAIN COMPUTERS - OR n.objectid ENDS WITH "-S-1-5-11" // AUTHENTICATED USERS - OR n.objectid ENDS WITH "-S-1-1-0" // EVERYONE - OR n.objectid ENDS WITH "S-1-5-32-545" // USERS - OR n.objectid ENDS WITH "S-1-5-32-546" // GUESTS - OR n.objectid ENDS WITH "S-1-5-7" // ANONYMOUS + MATCH p=(n:Group)-[r:AD_ATTACK_PATHS]->(:Base) + WHERE ( + n.objectid ENDS WITH "-513" // DOMAIN USERS + OR n.objectid ENDS WITH "-515" // DOMAIN COMPUTERS + OR n.objectid ENDS WITH "-S-1-5-11" // AUTHENTICATED USERS + OR n.objectid ENDS WITH "-S-1-1-0" // EVERYONE + OR n.objectid ENDS WITH "S-1-5-32-545" // USERS + OR n.objectid ENDS WITH "S-1-5-32-546" // GUESTS + OR n.objectid ENDS WITH "S-1-5-7" // ANONYMOUS + ) + AND NOT type(r) IN ["MemberOf", "ClaimSpecialIdentity"] RETURN p -revision: 1 +revision: 2 resources: acknowledgements: Martin Sohn Christensen, @martinsohndk From 1c925a9dc953187eac8b40ff6aa560ba780908d7 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Wed, 4 Nov 2026 12:22:43 +0100 Subject: [PATCH 49/55] All traversable edges except MemberOf: Principals With Direct Rights Assignment --- queries/Principals With Direct Rights Assignment.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/queries/Principals With Direct Rights Assignment.yml b/queries/Principals With Direct Rights Assignment.yml index 8141476..70bf3b9 100644 --- a/queries/Principals With Direct Rights Assignment.yml +++ b/queries/Principals With Direct Rights Assignment.yml @@ -5,15 +5,16 @@ platforms: Active Directory category: Active Directory Hygiene description: Identifies principals with rights assigned directly to them instead of to groups they are a member of. Active Directory best practice requires granting rights to groups, then adding users as group members. This role-based access control (RBAC) approach ensures permissions are easily auditable and manageable. Results include inherited rights, which must be modified at the parent container level. query: |- - MATCH p=(n:Base)-[r:GenericAll|GenericWrite|WriteOwner|WriteDacl|ForceChangePassword|AllExtendedRights|AddMember|AllowedToDelegate|AllowedToAct|AdminTo|CanPSRemote|CanRDP|ExecuteDCOM|AddSelf|DCSync|ReadLAPSPassword|ReadGMSAPassword|DumpSMSAPassword|AddAllowedToAct|WriteSPN|AddKeyCredentialLink|SyncLAPSPassword|WriteAccountRestrictions|WriteGPLink|ADCSESC1|ADCSESC3|ADCSESC4|ADCSESC6a|ADCSESC6b|ADCSESC9a|ADCSESC9b|ADCSESC10a|ADCSESC10b|ADCSESC13|WriteOwnerLimitedRights|OwnsLimitedRights|CanApplyGPO|ManageCA|ManageCertificates]->(:Base) - WHERE (n:User OR n:Computer) + MATCH p=(s:Base)-[r:AD_ATTACK_PATHS]->(t:Base) + WHERE (s:User OR s:Computer) + AND NOT type(r) IN ["MemberOf"] // Uncomment the below to only search enabled principals. - // AND n.enabled = true + // AND s.enabled = true RETURN p LIMIT 1000 -revision: 2 +revision: 3 resources: https://csrc.nist.gov/Projects/Role-Based-Access-Control acknowledgements: Martin Sohn Christensen, @martinsohndk From 4c1cf7ac62c676941b6bfdb3298846ad84f6faa8 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Wed, 4 Nov 2026 13:26:22 +0100 Subject: [PATCH 50/55] Create GPOs with computer configuration disabled linked to computers.yml --- ...ser configuration disabled linked to users.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 queries/GPOs with user configuration disabled linked to users.yml diff --git a/queries/GPOs with user configuration disabled linked to users.yml b/queries/GPOs with user configuration disabled linked to users.yml new file mode 100644 index 0000000..cd47971 --- /dev/null +++ b/queries/GPOs with user configuration disabled linked to users.yml @@ -0,0 +1,15 @@ +name: GPOs with user configuration disabled linked to users +guid: a1afd8a8-bdb2-466f-b876-c470521f5055 +prebuilt: false +platforms: Active Directory +category: Active Directory Hygiene +description: Shows GPOs with user configuration disabled (status 1) linked at the domain level with descendant enabled users. Returns up to 1000 GPLink and Contains paths to those users. Linked scope does not establish effective policy application and does not account for blocked inheritance, enforcement, security filtering, or WMI filtering. +query: |- + MATCH p = (g:GPO)-[:GPLink]->(:Domain)-[:Contains*1..]->(u:User) + WHERE toString(g.gpostatusraw) = '1' // user configuration disabled + AND u.enabled = true + RETURN p + LIMIT 1000 +revision: 2 +resources: +acknowledgements: Martin Sohn Christensen, @martinsohndk From 75e34447fd254263d02f38ae5999314e1635a2b6 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Wed, 4 Nov 2026 13:26:23 +0100 Subject: [PATCH 51/55] Create GPOs with computer configuration disabled linked to computers.yml --- ...configuration disabled linked to computers.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 queries/GPOs with computer configuration disabled linked to computers.yml diff --git a/queries/GPOs with computer configuration disabled linked to computers.yml b/queries/GPOs with computer configuration disabled linked to computers.yml new file mode 100644 index 0000000..56b296e --- /dev/null +++ b/queries/GPOs with computer configuration disabled linked to computers.yml @@ -0,0 +1,15 @@ +name: GPOs with computer configuration disabled linked to computers +guid: 82191294-bc75-4053-8646-f5be4ceb2a24 +prebuilt: false +platforms: Active Directory +category: Active Directory Hygiene +description: Shows GPOs with computer configuration disabled (status 2) linked at the domain level with descendant enabled computers. Returns up to 1000 GPLink and Contains paths to those computers. Linked scope does not establish effective policy application and does not account for blocked inheritance, enforcement, security filtering, or WMI filtering. +query: |- + MATCH p = (g:GPO)-[:GPLink]->(:Domain)-[:Contains*1..]->(c:Computer) + WHERE toString(g.gpostatusraw) = '2' // computer configuration disabled + AND c.enabled = true + RETURN p + LIMIT 1000 +revision: 2 +resources: +acknowledgements: Martin Sohn Christensen, @martinsohndk From 996d689d4d747d377a791054f61af647ce13758e Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Wed, 4 Nov 2026 13:26:25 +0100 Subject: [PATCH 52/55] Create GPOs with paths outside domain SYSVOL.yml --- .../GPOs with paths outside domain SYSVOL.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 queries/GPOs with paths outside domain SYSVOL.yml diff --git a/queries/GPOs with paths outside domain SYSVOL.yml b/queries/GPOs with paths outside domain SYSVOL.yml new file mode 100644 index 0000000..69c3b12 --- /dev/null +++ b/queries/GPOs with paths outside domain SYSVOL.yml @@ -0,0 +1,18 @@ +name: GPOs with paths outside domain SYSVOL +guid: 361c044c-e76b-4969-a27c-c08ca588f1b1 +prebuilt: false +platforms: Active Directory +category: Active Directory Hygiene +description: Finds GPOs whose collected GPC path does not contain \SYSVOL\ +query: |- + MATCH (g:GPO) + WHERE g.gpcpath IS NOT NULL + AND NOT (toUpper(g.gpcpath) CONTAINS ( + head(tail(split(toUpper(g.name), '@'))) + '\\SYSVOL\\' + )) + RETURN g + LIMIT 1000 +revision: 1 +resources: +- https://www.synacktiv.com/publications/gpoddity-exploiting-active-directory-gpos-through-ntlm-relaying-and-more#4-a-new-versatile-attack-vector-spoofing-gpo-location-through-the-gpcfilesyspath-attribute +acknowledgements: Martin Sohn Christensen, @martinsohndk From 3da79024663d28c2e6ee7541c22d61baa4c25c93 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Wed, 4 Nov 2026 13:39:45 +0100 Subject: [PATCH 53/55] Restore prebuilt unsupported operating systems query --- queries/Computers with unsupported operating systems.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/queries/Computers with unsupported operating systems.yml b/queries/Computers with unsupported operating systems.yml index 22f07fc..48c606e 100644 --- a/queries/Computers with unsupported operating systems.yml +++ b/queries/Computers with unsupported operating systems.yml @@ -3,13 +3,13 @@ guid: d06d3b14-0318-4fa9-9639-4b79ccaf3c2c prebuilt: true platforms: Active Directory category: Active Directory Hygiene -description: Identifies potentially unsupported Windows versions, including Windows 10. Windows 10 results do not verify ESU enrollment or LTSC support status. Unsupported Windows 11 releases are not detected. +description: query: |- MATCH (c:Computer) - WHERE c.operatingsystem =~ '(?i).*Windows.* (2000|2003|2008|2012|xp|vista|7|8|10|me|nt).*' + WHERE c.operatingsystem =~ '(?i).*Windows.* (2000|2003|2008|2012|xp|vista|7|8|me|nt).*' RETURN c LIMIT 100 -revision: 2 +revision: 1 resources: acknowledgements: From 04af1d4ac4afab407fe58a397c4a153a4c455599 Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Wed, 4 Nov 2026 13:45:08 +0100 Subject: [PATCH 54/55] Restore unsupported operating systems mapping scope --- docs/security-assessment-mapping.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/security-assessment-mapping.json b/docs/security-assessment-mapping.json index 7770b04..af8cb9c 100644 --- a/docs/security-assessment-mapping.json +++ b/docs/security-assessment-mapping.json @@ -733,8 +733,8 @@ "source": "PingCastle", "controls": [ { - "mapping_scope": "partial", - "mapping_scope_detail": "Flags Windows 10 without verifying ESU enrollment or LTSC support status. Does not detect unsupported Windows 11 releases.", + "mapping_scope": "exact", + "mapping_scope_detail": "", "id": "S-OS-W10", "name": "[M]Obsolete OS (Windows 10 or Windows 11)" }, From 46e31117a1c9ba9ec071643c7815ec1d90ebcb5a Mon Sep 17 00:00:00 2001 From: Martin Sohn Christensen Date: Wed, 4 Nov 2026 13:54:14 +0100 Subject: [PATCH 55/55] Add Netlogon collection check to DC registry health query --- queries/Collection health of DC Registry Data.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/queries/Collection health of DC Registry Data.yml b/queries/Collection health of DC Registry Data.yml index 3186341..b829fad 100644 --- a/queries/Collection health of DC Registry Data.yml +++ b/queries/Collection health of DC Registry Data.yml @@ -3,15 +3,19 @@ guid: 3f0fa2f3-fbdf-42c0-9e7d-97e689009161 prebuilt: false platforms: Active Directory category: Domain Information -description: BloodHound's ADCS analysis requires collecting CA registry data to increase accuracy/enable more edges. Collection by default requires SharpHound has Administrators membership. Requires SharpHound v2.3.5 or above. It only requires one misconfigured DC to potentially a full forest compromise by any principal. DCs returned by this query have an incomplete collection for one or more of the queried attributes. +description: Returns active DCs with incomplete collection of certificate binding enforcement or the Netlogon security descriptor. DC registry data supports BloodHound's ADCS analysis and assessment of Netlogon secure channel exceptions. Collection requires Administrators membership on the DC by default. Requires SharpHound v2.3.5 or above for certificate binding enforcement; Netlogon collection requires SharpHound v2.8.0 or above and BloodHound v8.3.0 or above. query: |- MATCH p=(:Domain)<-[:DCFor]-(c:Computer) - WHERE c.strongcertificatebindingenforcementraw IS NULL + WHERE ( + c.strongcertificatebindingenforcementraw IS NULL + OR c.vulnerablenetlogonsecuritydescriptorcollected IS NULL + OR c.vulnerablenetlogonsecuritydescriptorcollected = false + ) // Exclude inactive DCs AND c.enabled = true AND c.lastlogontimestamp > (datetime().epochseconds - (30 * 86400)) RETURN p -revision: 2 +revision: 3 resources: https://bloodhound.specterops.io/collect-data/sharphound-data-permissions#dc-registry acknowledgements: Martin Sohn Christensen, @martinsohndk