AD compliance score scope: What the Number Can Establish
AD compliance score scope determines what a number can support in an audit. In the etc-collector implementation reviewed on October 4, 2026, the score starts at 100 and subtracts points for four domain password-policy values and one count of enabled users carrying adminCount=1. It does not aggregate all findings or evaluate a complete compliance framework.
A score of 100 means that none of these five deduction conditions fired on the supplied inputs. It does not certify Active Directory, prove that every relevant object was collected, or establish that independent controls passed. Before using the number in a report, reconstruct its inputs and their collection evidence.
This analysis is pinned to collector revision 6a04c34b5ce01d3abf747c1d1c2f671eaa1d5e85, specifically public/internal/audit/detectors/ad/compliance/score.go, starting at Detect() on line 23. The examples below are calculations from that code, not measurements of a live domain. No Windows commands were executed against an environment for this article.
How the Five Deductions Work
With a non-null DomainInfo, each condition is evaluated independently. The fifth input is derived from the supplied Users collection. These are product thresholds; using them to reproduce a result does not establish that they are the organisation's approved baseline.
| Input consumed by the detector | Deduction condition | Points removed | Deduction key |
|---|---|---|---|
MinPwdLength | Below 14 | 10 | weak_password_length |
PwdHistoryLength | Below 24 | 5 | weak_password_history |
LockoutThreshold | Above 10 or equal to 0 | 5 | weak_lockout_policy |
MaxPwdAge, in collector whole days | Above 90 or equal to 0 | 5 | weak_password_age |
Number of supplied users with Disabled=false and AdminCount=true | Above 20 | 10 | excessive_admins |
Equality matters: length 14, history 24, lockout threshold 10, maximum age 90 days and count 20 do not trigger their respective deductions. The counter counts qualifying objects once; it does not calculate a ratio of administrators to users.
If DomainInfo is null, the detector skips all four policy conditions and subtracts 15 under no_domain_info. It then evaluates the same user-count condition. A null object is therefore a different calculation branch, not four confirmed policy failures.
Prove the Reachable Range
With DomainInfo present, the largest possible deduction is 10 + 5 + 5 + 5 + 10 = 35. The smallest is zero. Scores therefore range from 65 to 100, in five-point increments. The defensive clamp to zero is never reached by these conditions.
Without DomainInfo, the score is 85 when the qualifying user count is at most 20, or 75 when it exceeds 20. It cannot take other values in that branch.
For example, all four policy conditions firing with count 20 produces 75; count 21 produces 65. A null domain object with count 20 produces 85. These hypothetical cases explain why a better-looking number can accompany weaker policy evidence.
The finding includes interpretation bands such as 50-69 and 0-49. Only 65 is reachable within the former band; the latter is entirely unreachable. Values below 65 cannot arise from this calculation alone. Those display labels do not expand the detector's coverage or turn its upper band into a compliance verdict.
Detection: Reconcile the Inputs with Independent Reads
Start with the domain, the chosen domain controller, the collection time and the inventory scope. A fresh independent read from another DC or a narrower search base may describe different evidence. Record those differences before treating a mismatch as a detector error.
Microsoft documents Get-ADDefaultDomainPasswordPolicy as retrieving the default policy for a domain. This read-only example uses its documented -Identity and -Server parameters. Replace the illustrative domain and DC with the reviewed targets:
Get-ADDefaultDomainPasswordPolicy -Identity 'example.com' -Server 'dc01.example.com'
Retain the full returned object and compare minimum length, password history, lockout threshold and maximum age with the collector inputs. Preserve duration units: the provider converts the directory maximum-age interval into whole days. Do not compare a raw directory interval or a formatted duration string directly with the integer 90.
The collector reads these policy attributes from the domain object. It does not use each user's resultant password settings object (PSO) for this score. Microsoft documents Get-ADUserResultantPasswordPolicy for that separate account-level question. Keep such evidence outside the reproduced score rather than substituting a selected user's policy for the domain inputs.
Read adminCount Without Renaming the Population
Microsoft's adminCount attribute definition describes an object whose ACLs have been tightened by the system because of direct or transitive membership in an administrative group. That describes a protection history; it is not a current Domain Admins membership query.
The provider converts the exact LDAP string "1" into the user Boolean AdminCount=true. The detector then excludes disabled users. Although the finding's description and scope text say “Domain Admins count,” the actual predicate never resolves that group. Preserve the distinction when explaining the result.
Use the documented Get-ADUser search and property parameters to retain the underlying user records:
Get-ADUser -Filter * -SearchBase 'DC=example,DC=com' -Server 'dc01.example.com' -Properties adminCount,Enabled
This is an adapted documentation example, not an executed inventory. From the returned records, reconcile the enabled users whose raw adminCount equals 1 with the collector's qualifying population. Retain object identities and exceptions, not just the final total. A missing attribute, a restricted base or a collection limit needs qualification before a low count is interpreted.
Build a Calculation and Provenance Sheet
For an actual audit, populate the following record with observed values. “Collection confirmed” means the required response and attributes were read successfully for the stated scope; a non-null structure alone does not establish that.
| Input | Collector source | Evidence required to confirm collection | Calculation to reproduce |
|---|---|---|---|
| Minimum length | Domain minPwdLength mapped to MinPwdLength | Domain object identity, returned attribute, successful parsing, DC and time | Subtract 10 when below 14 |
| History | Domain pwdHistoryLength mapped to PwdHistoryLength | Same domain identity, attribute and parse evidence | Subtract 5 when below 24 |
| Lockout threshold | Domain lockoutThreshold | Attribute response and parse evidence; qualify any zero | Subtract 5 when above 10 or zero |
| Maximum age | Domain maxPwdAge converted to whole days | Raw value, conversion outcome and units; qualify any zero | Subtract 5 when above 90 or zero |
| Enabled adminCount population | Supplied Users, parsed adminCount and disabled flag | Search base, population/filter, limits, returned attributes and object identities | Subtract 10 when count exceeds 20 |
Add the actual value, collection outcome, independent value and resulting deduction to each row. Where evidence is unavailable, write “unknown” in the evidence column while preserving the numerical input the code consumed. Do not silently replace the input with a guessed value.
Remediation: Resolve Evidence Gaps Before Changing Policy
First reconcile the calculation. Preserve the finding's score, deduction map, user statistics and collector revision. Recompute the result with the supplied inputs and identify whether the domain object was present, partial or null. This separates an arithmetic discrepancy from an incomplete observation.
Next qualify collection. A zero can represent a configured value or an absent/unparsed value in this provider. Obtain the relevant domain response and an independent read on the recorded DC. For users, reconcile search scope and limits with the intended population. A successful GetUsers call is necessary evidence, but does not by itself demonstrate that an intentionally limited inventory covers the whole domain.
If collection is incomplete, address the specific collection problem and run a fresh audit. Preserve the previous result as an incomplete observation. Do not describe the move from 85 to 100 after recovering domain information as four remediated policy controls without before-and-after policy evidence.
Then identify the organisation's baseline and its edition, owner and exceptions. Our comparison of ANSSI and CIS password-policy predicates explains why different checks can disagree on overlapping attributes. This score's five deductions are a separate product calculation; they are not a third framework baseline.
For a confirmed configuration gap, apply the approved policy change through the established administration process, retain its change record, repeat the independent read and re-audit the same scope. For an adminCount discrepancy, investigate the actual objects and current privileged memberships separately. Clearing the marker merely to lower the counter does not establish that privileges or ACL protection are correct.
Finally attach independent control evidence. Our mapping of AD and Azure compliance requirements to named identity controls provides context for that work. Logging, protocol protections, access governance and Entra controls need their own results. This score does not inspect those controls, and cannot fill their evidence gaps.
How EtcSec Detects This
COMPLIANCE_SCORE is the sole detector covered here. Its Detect() reads DomainInfo and Users, applies the five conditions above, and always returns one Info finding with Count=1, including at score 100. Count 1 denotes the returned score finding, not one affected user or one failed control.
The details include score, maxScore=100, deductions, totalUsers, enabledUsers and adminCount. User totals describe the supplied collection. They do not establish full directory coverage.
The provider paths explain the limits:
public/internal/providers/ldap/client.go:569creates a domain-information object before querying RootDSE and the domain. Failed or empty responses can leave a partial object with zero values. The four policy values come from the domain object, not a resultant PSO per user.public/internal/providers/ldap/parser.go:155defines userAdminCountusing the exact string comparisonadminCount == "1". Its integer helper returns zero for absent values and ignores integer parsing errors.public/internal/audit/engine.go:512treats a returnedGetDomainInfoerror as non-fatal and supplies null domain information. That branch yields 85 before the optional 10-point adminCount deduction. In contrast, the earlierGetUserserror path aborts collection.- The calculation's reachable values are 65–100 with a domain object, or 75/85 without it. A non-null object is not proof of successful attribute collection, and interpretation bands below 65 are unreachable by this detector.
All code references above are pinned to the reviewed revision. Report the output as a partial product indicator supported by qualified inputs. A score of 100 establishes only that these five deduction conditions did not fire; overall compliance and certification require independently defined controls and evidence.
Explore the identity security pages that support this topic
