Skip to content

fix(postprocess): Fix multi-forest ADCS false positives BED-5572#2900

Merged
cweidenkeller merged 1 commit into
mainfrom
BED-5572
Jun 24, 2026
Merged

fix(postprocess): Fix multi-forest ADCS false positives BED-5572#2900
cweidenkeller merged 1 commit into
mainfrom
BED-5572

Conversation

@cweidenkeller

@cweidenkeller cweidenkeller commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Description

Fix multi-forest false positives for ADCSx edges being created.

Motivation and Context

Resolves BED-5572

Why is this change required? What problem does it solve?

Multi forest ADCS were generating false positives as certificates were not tracked cross forest this PR fixes that issue

How Has This Been Tested?

Added both unit and integration tests and ensured no previous tests failed

Please describe in detail how you tested your changes.
Include details of your testing environment, and the tests you ran to
see how your change affects other areas of the code, etc.

Screenshots (optional):

Types of changes

  • Chore (a change that does not modify the application functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Database Migrations

Checklist:

Summary by CodeRabbit

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Updated hosted-chain domain results for Enterprise CAs to be forest-aware when the CA’s scope can be determined, ensuring domains are restricted to the CA’s own forest.
    • Enterprise CAs are now omitted from hosted-chain results if any enabled hosting computer exists only in a foreign forest.
    • When forest scope can’t be determined, results fall back to prior hosting-based behavior.
  • Tests
    • Added unit and integration coverage for in-forest vs cross-forest hosting behavior and fallback scenarios.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

ADCSCache gains a hasInForestHostingComputer field and a domainsBySID index in BuildCache to support per-CA forest resolution via resolveEnterpriseCAForest. Hosting-computer detection and chained-domain queries in GetECAHostedChainedDomains are updated to enforce forest boundaries: CAs without an enabled host in their own resolved forest are excluded. Unit tests validate forest-known and forest-unknown filtering scenarios with in-forest vs. cross-forest hosting. Integration tests build two-forest graph fixtures with certificate chains and hosting links to assert CA and domain retention behavior.

Changes

ADCS Forest-Scoping

Layer / File(s) Summary
ADCSCache struct extensions and forest resolver
packages/go/analysis/ad/adcscache.go
Adds hasInForestHostingComputer field to track per-CA in-forest hosting status, initializes it in NewADCSCache, builds a domainsBySID index during BuildCache to map domain ObjectID SIDs, and introduces resolveEnterpriseCAForest helper to expand a CA's forest via SameForestTrust closure from the CA's DomainSID.
Forest-aware hosting and domain filtering
packages/go/analysis/ad/adcscache.go
BuildCache's hosting-computer loop sets hasInForestHostingComputer only when an enabled host's DomainSID maps into the CA's resolved forest. GetECAHostedChainedDomains skips CAs without an in-forest host (when forest is known) or without any host (when forest is unknown), filtering chained domains to the CA's forest boundary.
Unit tests for forest filtering
packages/go/analysis/ad/adcscache_test.go
Helpers duplexOf, newForestHostingCache, and adcsForestHostingNodes construct deterministic ADCSCache states with forest scoping inputs. TestGetECAHostedChainedDomains_ForestHosting covers forest-known in-forest/cross-forest hosting and forest-unknown scenarios. TestGetChainedDomains_IgnoresForestHosting validates that GetChainedDomains bypasses hosting guards.
Integration tests via graph fixtures
packages/go/analysis/ad/adcs_forest_integration_test.go
Helpers addEnabledHostingComputer and linkEnterpriseCAToDomain establish full ADCS certificate-chain relationships. TestADCSForestScoping_UsesHostForestECAAndKeepsCrossForestDomain asserts in-forest CA retention with both forest domains; TestADCSForestScoping_DropsCAWithOnlyCrossForestHost asserts CA omission when only cross-forest hosting exists.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the fix for multi-forest ADCS false positives and references the relevant ticket (BED-5572), directly matching the changeset's primary objective.
Description check ✅ Passed The description addresses the template requirements with clear issue context (BED-5572), motivation (fixing multi-forest certificate tracking), testing approach, and completed checklist items.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch BED-5572

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added the go Pull requests that update go code label Jun 17, 2026
@cweidenkeller cweidenkeller self-assigned this Jun 17, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/go/analysis/ad/adcscache.go (1)

338-389: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Resolve forest state independently from the hosting-computer lookup.

GetChainedDomains() relies on ecaForestDomains, but a HostsCAService fetch error skips forest resolution entirely, so Line 597 treats the CA as forest-unknown and keeps cross-forest chained domains. Move forest cache population before the host query, and clear entries when resolution is unknown to avoid stale map-presence behavior on rebuilds.

🐛 Proposed fix
+			// Resolve and cache the CA forest independently from HostsCAService so
+			// GetChainedDomains can still apply forest scoping if host lookup fails.
+			forestDomains, forestKnown, forestResolutionErr := resolveEnterpriseCAForest(tx, eca, domainsBySID)
+			if forestResolutionErr != nil {
+				slog.WarnContext(
+					ctx,
+					"Error resolving forest for enterprise ca",
+					slog.Uint64("enterprise_ca", uint64(eca.ID)),
+					attr.Error(forestResolutionErr),
+				)
+			}
+			if forestKnown {
+				s.ecaForestDomains[eca.ID] = forestDomains
+			} else {
+				delete(s.ecaForestDomains, eca.ID)
+				delete(s.hasInForestHostingComputer, eca.ID)
+			}
+
 			if hostingComputers, err := fetchFirstDegreeNodes(tx, eca, ad.HostsCAService); err != nil {
 				slog.ErrorContext(
 					ctx,
 					"Error fetching hosting computers for enterprise ca",
 					slog.Uint64("enterprise_ca", uint64(eca.ID)),
 					attr.Error(err),
 				)
 			} else {
-				// Resolve the forest the CA itself belongs to (the SameForestTrust
-				// closure of the CA's domain). When this can't be determined we fall
-				// back to forest-agnostic behavior to avoid dropping real findings.
-				forestDomains, forestKnown, err := resolveEnterpriseCAForest(tx, eca, domainsBySID)
-				if err != nil {
-					slog.WarnContext(
-						ctx,
-						"Error resolving forest for enterprise ca",
-						slog.Uint64("enterprise_ca", uint64(eca.ID)),
-						attr.Error(err),
-					)
-				}
-
 				var (
 					hasHostingComputer = false
 					hostInForest       = false
 				)
 
@@
 				s.hasHostingComputer[eca.ID] = hasHostingComputer
 				if forestKnown {
-					s.ecaForestDomains[eca.ID] = forestDomains
 					s.hasInForestHostingComputer[eca.ID] = hostInForest
+				} else {
+					delete(s.hasInForestHostingComputer, eca.ID)
 				}
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/go/analysis/ad/adcscache.go` around lines 338 - 389, The forest
resolution logic is currently nested inside the else block that only executes
when the hosting computer fetch succeeds, which means if fetchFirstDegreeNodes
fails, the forest is never resolved and ecaForestDomains is never populated.
Move the resolveEnterpriseCAForest call to execute independently before checking
the hosting computer fetch result, so that forest domains are resolved and
cached in s.ecaForestDomains regardless of whether the HostsCAService query
succeeds or fails. This ensures GetChainedDomains can reliably check forestKnown
status without relying on the hosting computer lookup outcome.
🧹 Nitpick comments (1)
packages/go/analysis/ad/adcscache_test.go (1)

55-60: ⚡ Quick win

Remove named returns from adcsForestFilterNodes (Line 55).

This helper uses named returns plus a naked return, which violates the Go guideline for this repo.

Proposed fix
-func adcsForestFilterNodes() (eca, inForestDomain, foreignDomain *graph.Node) {
-	eca = graph.NewNode(10, graph.NewProperties(), ad.EnterpriseCA)
-	inForestDomain = graph.NewNode(1, graph.NewProperties(), ad.Domain)
-	foreignDomain = graph.NewNode(2, graph.NewProperties(), ad.Domain)
-	return eca, inForestDomain, foreignDomain
+func adcsForestFilterNodes() (*graph.Node, *graph.Node, *graph.Node) {
+	enterpriseCA := graph.NewNode(10, graph.NewProperties(), ad.EnterpriseCA)
+	inForestDomain := graph.NewNode(1, graph.NewProperties(), ad.Domain)
+	foreignDomain := graph.NewNode(2, graph.NewProperties(), ad.Domain)
+	return enterpriseCA, inForestDomain, foreignDomain
 }

As per coding guidelines, “Named returns are not allowed; all return variables must be defined in the function.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/go/analysis/ad/adcscache_test.go` around lines 55 - 60, The
adcsForestFilterNodes function uses named returns in its signature (eca,
inForestDomain, foreignDomain *graph.Node) combined with a naked return
statement, which violates the repo's Go coding guidelines. Remove the named
return variables from the function signature and specify only the return types
(*graph.Node, *graph.Node, *graph.Node), keeping the explicit return statement
with the three variables as is. This will make the function compliant with the
guideline that all return variables must be defined in the function body.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/go/analysis/ad/adcscache.go`:
- Around line 338-389: The forest resolution logic is currently nested inside
the else block that only executes when the hosting computer fetch succeeds,
which means if fetchFirstDegreeNodes fails, the forest is never resolved and
ecaForestDomains is never populated. Move the resolveEnterpriseCAForest call to
execute independently before checking the hosting computer fetch result, so that
forest domains are resolved and cached in s.ecaForestDomains regardless of
whether the HostsCAService query succeeds or fails. This ensures
GetChainedDomains can reliably check forestKnown status without relying on the
hosting computer lookup outcome.

---

Nitpick comments:
In `@packages/go/analysis/ad/adcscache_test.go`:
- Around line 55-60: The adcsForestFilterNodes function uses named returns in
its signature (eca, inForestDomain, foreignDomain *graph.Node) combined with a
naked return statement, which violates the repo's Go coding guidelines. Remove
the named return variables from the function signature and specify only the
return types (*graph.Node, *graph.Node, *graph.Node), keeping the explicit
return statement with the three variables as is. This will make the function
compliant with the guideline that all return variables must be defined in the
function body.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 9150cb7a-1299-4679-9e66-79e9d9d1f176

📥 Commits

Reviewing files that changed from the base of the PR and between 2c568f4 and 31d5199.

📒 Files selected for processing (3)
  • packages/go/analysis/ad/adcs_forest_integration_test.go
  • packages/go/analysis/ad/adcscache.go
  • packages/go/analysis/ad/adcscache_test.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/go/analysis/ad/adcscache_test.go`:
- Around line 55-60: Remove the named return values from the function signature
of adcsForestFilterNodes. Replace the named return parameters (eca,
inForestDomain, foreignDomain *graph.Node) with unnamed return types
(*graph.Node, *graph.Node, *graph.Node). Keep the return statement unchanged
since it already explicitly returns the three variables by name.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 82ea7a02-126e-4860-9959-e20123e3012c

📥 Commits

Reviewing files that changed from the base of the PR and between 31d5199 and f5126fb.

📒 Files selected for processing (3)
  • packages/go/analysis/ad/adcs_forest_integration_test.go
  • packages/go/analysis/ad/adcscache.go
  • packages/go/analysis/ad/adcscache_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/go/analysis/ad/adcscache.go

Comment on lines +55 to +60
func adcsForestFilterNodes() (eca, inForestDomain, foreignDomain *graph.Node) {
eca = graph.NewNode(10, graph.NewProperties(), ad.EnterpriseCA)
inForestDomain = graph.NewNode(1, graph.NewProperties(), ad.Domain)
foreignDomain = graph.NewNode(2, graph.NewProperties(), ad.Domain)
return eca, inForestDomain, foreignDomain
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove named return values from adcsForestFilterNodes.

This helper currently uses named returns, which violates the Go guideline for this repository.

Suggested fix
-func adcsForestFilterNodes() (eca, inForestDomain, foreignDomain *graph.Node) {
-	eca = graph.NewNode(10, graph.NewProperties(), ad.EnterpriseCA)
-	inForestDomain = graph.NewNode(1, graph.NewProperties(), ad.Domain)
-	foreignDomain = graph.NewNode(2, graph.NewProperties(), ad.Domain)
-	return eca, inForestDomain, foreignDomain
+func adcsForestFilterNodes() (*graph.Node, *graph.Node, *graph.Node) {
+	eca := graph.NewNode(10, graph.NewProperties(), ad.EnterpriseCA)
+	inForestDomain := graph.NewNode(1, graph.NewProperties(), ad.Domain)
+	foreignDomain := graph.NewNode(2, graph.NewProperties(), ad.Domain)
+	return eca, inForestDomain, foreignDomain
 }

As per coding guidelines: "**/*.go: Named returns are not allowed; all return variables must be defined in the function."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func adcsForestFilterNodes() (eca, inForestDomain, foreignDomain *graph.Node) {
eca = graph.NewNode(10, graph.NewProperties(), ad.EnterpriseCA)
inForestDomain = graph.NewNode(1, graph.NewProperties(), ad.Domain)
foreignDomain = graph.NewNode(2, graph.NewProperties(), ad.Domain)
return eca, inForestDomain, foreignDomain
}
func adcsForestFilterNodes() (*graph.Node, *graph.Node, *graph.Node) {
eca := graph.NewNode(10, graph.NewProperties(), ad.EnterpriseCA)
inForestDomain := graph.NewNode(1, graph.NewProperties(), ad.Domain)
foreignDomain := graph.NewNode(2, graph.NewProperties(), ad.Domain)
return eca, inForestDomain, foreignDomain
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/go/analysis/ad/adcscache_test.go` around lines 55 - 60, Remove the
named return values from the function signature of adcsForestFilterNodes.
Replace the named return parameters (eca, inForestDomain, foreignDomain
*graph.Node) with unnamed return types (*graph.Node, *graph.Node, *graph.Node).
Keep the return statement unchanged since it already explicitly returns the
three variables by name.

Source: Coding guidelines

@cweidenkeller cweidenkeller force-pushed the BED-5572 branch 2 times, most recently from fa69c5f to 54d5fbf Compare June 17, 2026 21:14
@JonasBK

JonasBK commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

dataset.zip
for testing

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/go/analysis/ad/adcs_forest_integration_test.go`:
- Around line 91-97: The test is setting up chain validity for
hostForestEnterpriseCA through linkEnterpriseCAToDomain calls (lines 91-93), but
copiedEnterpriseCA is not being configured as chain-valid. This means the
exclusion assertion at line 111 might pass for the wrong reason (due to an
invalid chain) rather than proving the actual cross-forest host filtering logic.
Add chain validity setup for copiedEnterpriseCA by calling
linkEnterpriseCAToDomain with copiedEnterpriseCA in a similar manner to how
hostForestEnterpriseCA is configured, ensuring both domains are properly linked
so the subsequent exclusion test validates the correct behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: cbb19cde-a5b4-4b74-8377-f6c34e913ce7

📥 Commits

Reviewing files that changed from the base of the PR and between 54d5fbf and 74e4f15.

📒 Files selected for processing (3)
  • packages/go/analysis/ad/adcs_forest_integration_test.go
  • packages/go/analysis/ad/adcscache.go
  • packages/go/analysis/ad/adcscache_test.go

Comment on lines +91 to +97
linkEnterpriseCAToDomain(testContext, hostForestEnterpriseCA, rootCA, domainA, domainASID)
// ...and a cross-forest chain into forest B (shared ADCS).
linkEnterpriseCAToDomain(testContext, hostForestEnterpriseCA, rootCA, domainB, domainBSID)

host := addEnabledHostingComputer(testContext, "HostA", domainASID, hostForestEnterpriseCA)
testContext.NewRelationship(host, copiedEnterpriseCA, ad.HostsCAService)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make copiedEnterpriseCA chain-valid so the exclusion assertion proves forest-host filtering.

Right now, Line 91-Line 93 wire chain validity only for hostForestEnterpriseCA. If copiedEnterpriseCA is not chain-valid, Line 111 can pass for the wrong reason (missing chain instead of cross-forest host rejection).

Suggested fix
 			// Valid cert chain to forest A (the CA's own forest)...
 			linkEnterpriseCAToDomain(testContext, hostForestEnterpriseCA, rootCA, domainA, domainASID)
 			// ...and a cross-forest chain into forest B (shared ADCS).
 			linkEnterpriseCAToDomain(testContext, hostForestEnterpriseCA, rootCA, domainB, domainBSID)
+
+			// Ensure copied CA is also chain-valid so exclusion is attributable to
+			// forest-host gating, not missing chain relationships.
+			testContext.NewRelationship(copiedEnterpriseCA, rootCA, ad.EnterpriseCAFor)
+			linkEnterpriseCAToDomain(testContext, copiedEnterpriseCA, rootCA, domainA, domainASID)
+			linkEnterpriseCAToDomain(testContext, copiedEnterpriseCA, rootCA, domainB, domainBSID)
 
 			host := addEnabledHostingComputer(testContext, "HostA", domainASID, hostForestEnterpriseCA)
 			testContext.NewRelationship(host, copiedEnterpriseCA, ad.HostsCAService)

Also applies to: 110-112

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/go/analysis/ad/adcs_forest_integration_test.go` around lines 91 -
97, The test is setting up chain validity for hostForestEnterpriseCA through
linkEnterpriseCAToDomain calls (lines 91-93), but copiedEnterpriseCA is not
being configured as chain-valid. This means the exclusion assertion at line 111
might pass for the wrong reason (due to an invalid chain) rather than proving
the actual cross-forest host filtering logic. Add chain validity setup for
copiedEnterpriseCA by calling linkEnterpriseCAToDomain with copiedEnterpriseCA
in a similar manner to how hostForestEnterpriseCA is configured, ensuring both
domains are properly linked so the subsequent exclusion test validates the
correct behavior.

Comment thread packages/go/analysis/ad/adcs_forest_integration_test.go Outdated
Comment thread packages/go/analysis/ad/adcscache.go Outdated
Comment thread packages/go/analysis/ad/adcscache.go Outdated
@kpowderly

kpowderly commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Left a couple comments but looks good otherwise! I know you had planned to go over this for demo tomorrow but lemme know when you're ready for a quick re-review and I'll ✅

@cweidenkeller cweidenkeller merged commit d8bb6c2 into main Jun 24, 2026
12 checks passed
@cweidenkeller cweidenkeller deleted the BED-5572 branch June 24, 2026 20:19
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

go Pull requests that update go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants