Documentation of the context-aware risk scoring system used in adPEAS v2 HTML reports.
Maintenance: All scoring values are centrally defined in:
- PowerShell:
src/modules/Core/adPEAS-ScoringDefinitions.ps1- JavaScript: Auto-generated from PowerShell during build via
ConvertTo-ScoringJavaScript
The adPEAS v2 scoring system calculates realistic risk scores based on multiple contextual factors rather than simple title-based categorization. This enables security teams to prioritize remediation efforts based on actual exploitability and potential impact.
FINAL_SCORE = (BASE × IMPACT × EXPLOITABILITY × SECURITY) + CORRELATION
| Component | Range | Description |
|---|---|---|
| BASE | 0-100 | Intrinsic severity of the finding type |
| IMPACT | 1.0-2.0 | Multiplier based on account privilege tier |
| EXPLOITABILITY | 0.6-1.6 | Modifier based on password age, policy, encryption |
| SECURITY | 0.1-1.0 | Reduction factor for mitigating controls |
| CORRELATION | 0-15 | Bonus for accounts appearing in multiple findings |
Final score is clamped to 0-100.
Base scores represent the intrinsic risk of each vulnerability type. The actual score is then modified by context.
These findings can lead to immediate domain compromise.
| Keyword | Base Score | Check Module | Description |
|---|---|---|---|
dcsync |
100 | Get-DangerousACLs | DCSync rights - immediate domain compromise |
replication rights |
100 | Get-DangerousACLs | Same as DCSync |
esc1 |
90 | Get-ADCSVulnerabilities | ADCS ESC1 - enrollee supplies subject + client auth |
esc2 |
80 | Get-ADCSVulnerabilities | ADCS ESC2 - any purpose or SubCA template |
esc3 |
75 | Get-ADCSVulnerabilities | ADCS ESC3 - certificate request agent |
esc4 |
70 | Get-ADCSVulnerabilities | ADCS ESC4 - dangerous template ACL |
esc13 |
70 | Get-ADCSVulnerabilities | ADCS ESC13 - issuance policy linked to AD group |
esc6 |
65 | Get-ADCSVulnerabilities | ADCS ESC6 - EDITF_ATTRIBUTESUBJECTALTNAME2 on CA |
esc7 |
65 | Get-ADCSVulnerabilities | ADCS ESC7 - dangerous CA permissions |
esc8 |
70 | Get-ADCSVulnerabilities | ADCS ESC8 - NTLM relay to web enrollment |
esc9 |
60 | Get-ADCSVulnerabilities | ADCS ESC9 - template without security extension |
esc10 |
60 | Get-ADCSVulnerabilities | ADCS ESC10 - weak certificate mapping |
esc15 |
30 | Get-ADCSVulnerabilities | ADCS ESC15 - schema v1 template with enrollee subject |
unconstrained delegation |
85 | Get-UnconstrainedDelegation | Can capture TGTs |
sid history |
80 | Get-SIDHistoryInjection | SID History injection |
Findings that expose or allow access to credentials.
| Keyword | Base Score | Check Module | Description |
|---|---|---|---|
laps credential |
55 | Get-LAPSCredentialAccess | LAPS password readable |
laps access |
45 | Get-LAPSPermissions | LAPS password read permissions |
laps password |
55 | Get-LAPSCredentialAccess | LAPS password readable |
gpp password |
50 | Get-CredentialExposure | Group Policy Preferences password |
cpassword |
50 | Get-CredentialExposure | GPP cpassword attribute |
cleartext credential |
50 | Get-CredentialExposure | Cleartext credentials found |
plaintext credential |
50 | Get-CredentialExposure | Plaintext credentials found |
credential exposure |
45 | Get-CredentialExposure | Generic credential exposure |
autoadminlogon |
45 | Get-CredentialExposure | AutoAdminLogon credentials |
password in script |
45 | Get-GPOScheduledTasks | Password in scheduled task script |
sensitive information |
40 | Get-CredentialExposure | Sensitive data exposure |
net use |
40 | Get-CredentialExposure | Net use with credentials |
Findings that require offline password cracking.
| Keyword | Base Score | Check Module | Description |
|---|---|---|---|
kerberoastable |
35 | Get-KerberoastableAccounts | Kerberoastable service account |
kerberoast |
35 | Get-KerberoastableAccounts | Kerberoastable service account |
asrep roast |
30 | Get-ASREPRoastableAccounts | AS-REP roastable account |
as-rep roast |
30 | Get-ASREPRoastableAccounts | AS-REP roastable account |
preauth not required |
30 | Get-ASREPRoastableAccounts | Pre-authentication disabled |
Findings related to Kerberos delegation misconfigurations.
| Keyword | Base Score | Check Module | Description |
|---|---|---|---|
rbcd |
55 | Get-ResourceBasedConstrainedDelegation | Resource-Based Constrained Delegation |
resource-based constrained |
55 | Get-ResourceBasedConstrainedDelegation | RBCD abuse |
constrained delegation |
50 | Get-ConstrainedDelegation | Constrained delegation configured |
Findings related to dangerous Access Control List entries.
| Keyword | Base Score | Check Module | Description |
|---|---|---|---|
genericall |
60 | Get-DangerousACLs | Full control over object |
writedacl |
55 | Get-DangerousACLs | Can modify permissions |
writeowner |
55 | Get-DangerousACLs | Can change ownership |
genericwrite |
50 | Get-DangerousACLs | Can write to object |
password reset |
50 | Get-PasswordResetRights | Can reset passwords |
reset password |
50 | Get-PasswordResetRights | Can reset passwords |
gpo permission |
50 | Get-GPOPermissions | Can modify GPOs |
dangerous permission |
45 | Get-DangerousACLs | Other dangerous ACL |
laps permission |
45 | Get-LAPSPermissions | Can read LAPS passwords |
gpo link |
45 | Get-GPOPermissions | Can link GPOs |
ou permission |
40 | Get-DangerousOUPermissions | Dangerous OU permissions |
Findings related to insecure configurations.
| Keyword | Base Score | Check Module | Description |
|---|---|---|---|
local admin |
45 | Get-GPOLocalGroupMembership | Added to local admins via GPO |
computer owner |
40 | Get-NonDefaultComputerOwners | Non-default computer owner |
local group |
40 | Get-GPOLocalGroupMembership | GPO local group membership |
password not required |
35 | Get-PasswordNotRequired | PASSWD_NOTREQD flag |
add computer |
35 | Get-AddComputerRights | Can add computers to domain |
laps not configured |
35 | Get-LAPSConfiguration | LAPS not deployed |
reversible encryption |
30 | Get-AdminReversibleEncryption | Reversible encryption enabled |
inactive admin |
30 | Get-InactiveAdminAccounts | Inactive admin account |
orphaned admin |
30 | Get-InactiveAdminAccounts | Orphaned admin account |
smb signing |
30 | Get-SMBSigningStatus | SMB signing not required |
machineaccountquota |
15 | Get-AddComputerRights | MachineAccountQuota > 0 |
kds root key |
30 | Get-ManagedServiceAccountSecurity | KDS root key issues |
password never expires |
25 | Get-AdminPasswordNeverExpires | Password never expires |
ldap signing |
25 | Get-LDAPConfiguration | LDAP signing not required |
channel binding |
25 | Get-LDAPConfiguration | Channel binding not required |
Findings related to service account security.
| Keyword | Base Score | Check Module | Description |
|---|---|---|---|
gmsa password readable |
55 | Get-ManagedServiceAccountSecurity | gMSA password retrievable |
managed service account |
25 | Get-ManagedServiceAccountSecurity | MSA configuration issue |
smsa |
20 | Get-ManagedServiceAccountSecurity | sMSA configuration issue |
Findings related to infrastructure components.
| Keyword | Base Score | Check Module | Description |
|---|---|---|---|
exchange |
45 | Get-ExchangeInfrastructure | Exchange infrastructure finding |
unsupported os |
40 | Get-OutdatedComputers | Unsupported operating system |
scom |
35 | Get-SCOMInfrastructure | SCOM infrastructure |
sccm site hierarchy |
30 | Get-SCCMInfrastructure | SCCM multi-site hierarchy |
sccm pxe |
25 | Get-SCCMInfrastructure | PXE boot server exposure |
sccm client |
20 | Get-SCCMInfrastructure | SCCM client deployment scope |
aging os |
15 | Get-OutdatedComputers | Aging operating system (nearing EOL) |
unix password |
30 | Get-UnixPasswordAccounts | Unix password attributes |
unixuserpwd |
30 | Get-UnixPasswordAccounts | unixUserPassword attribute |
Findings related to LAPS deployment.
| Keyword | Base Score | Check Module | Description |
|---|---|---|---|
laps not configured |
35 | Get-LAPSConfiguration | LAPS missing |
laps |
20 | Get-LAPSConfiguration | LAPS informational |
Low-severity informational findings.
| Keyword | Base Score | Check Module | Description |
|---|---|---|---|
privileged group |
15 | Get-PrivilegedGroupMembers | Privileged group membership |
trust |
15 | Get-DomainTrusts | Domain trust |
group member |
10 | Get-PrivilegedGroupMembers | Group membership info |
protected users |
10 | Get-ProtectedUsersStatus | Protected Users status |
password policy |
10 | Get-DomainPasswordPolicy | Password policy info |
domain controller |
10 | Get-InfrastructureServers | Domain controller info |
infrastructure server |
10 | Get-InfrastructureServers | Infrastructure server info |
When no keyword matches, the system uses severity-based fallbacks:
| Severity Class | Fallback Score |
|---|---|
finding |
35 |
hint |
15 |
note |
0 |
secure |
0 |
The impact multiplier adjusts the base score based on the privilege level of the affected account.
adPEAS uses the Microsoft Privileged Access Model for tier classification:
| Tier | Multiplier | SID Pattern | Groups | Scope |
|---|---|---|---|---|
| Tier 0 | ×2.0 | -512, -519, -518 |
Domain Admins, Enterprise Admins, Schema Admins | Domain Controllers |
| Tier 1 | ×1.5 | -548, -549, -551, -550 |
Account Operators, Server Operators, Backup Operators, Print Operators | Servers |
| Tier 2 | ×1.2 | Other privileged | Other administrative groups | Workstations |
| None | ×1.0 | Standard | Regular user accounts | - |
Kerberoastable Domain Admin:
Base: 35 (kerberoastable)
× Impact: 2.0 (Tier 0 - Domain Admin)
= 70 (before other modifiers)
Kerberoastable Standard User:
Base: 35 (kerberoastable)
× Impact: 1.0 (None)
= 35 (before other modifiers)
The exploitability modifier adjusts the score based on factors that affect how easily the vulnerability can be exploited.
Password age is evaluated relative to the domain's maxPwdAge policy, not as an absolute value.
| Policy Multiples | Modifier | Description |
|---|---|---|
| ≥10× maxPwdAge | ×1.6 | Extremely old password |
| ≥5× maxPwdAge | ×1.4 | Very old password |
| ≥3× maxPwdAge | ×1.3 | Old password |
| ≥2× maxPwdAge | ×1.2 | Moderately old |
| ≥1× maxPwdAge | ×1.1 | Over policy limit |
| <1× maxPwdAge | ×1.0 | Within policy |
Example: If maxPwdAge = 90 days:
- Password 900 days old → 10× policy → ×1.6 modifier
- Password 180 days old → 2× policy → ×1.2 modifier
- Password 45 days old → 0.5× policy → ×1.0 modifier
The domain's password policy affects cracking-based attack scores.
| Min Length | Modifier | Description |
|---|---|---|
| <8 chars | ×1.4 | Very weak - easy to crack |
| 8-11 chars | ×1.2 | Weak |
| 12-15 chars | ×1.0 | Standard |
| ≥16 chars | ×0.8 | Strong - harder to crack |
| Complexity | Modifier | Description |
|---|---|---|
| Disabled | ×1.25 | Weak passwords likely |
| Enabled | ×1.0 | Standard complexity |
Note: Complexity is determined from the pwdProperties attribute (bit 0 = DOMAIN_PASSWORD_COMPLEX).
| Encryption | Modifier | Description |
|---|---|---|
| RC4 only (etype 23) | ×1.3 | Fast to crack |
| AES128 (etype 17) | ×1.0 | Standard |
| AES256 (etype 18) | ×0.9 | Slower to crack |
Security modifiers reduce the score when mitigating controls are in place.
| Flag | Modifier | Applies To | Description |
|---|---|---|---|
ACCOUNTDISABLE |
×0.1 | All findings | Account is disabled |
LOCKOUT |
×0.3 | All findings | Account is locked out |
SMARTCARD_REQUIRED |
×0.15 | Cracking attacks | Password is random/unknown |
PASSWORD_EXPIRED |
×0.7 | Credential attacks | Password may be changed soon |
USE_DES_KEY_ONLY |
×1.3 | Kerberos attacks | Weak encryption (increases risk) |
NOT_DELEGATED |
×0.3 | Delegation attacks | Delegation protection enabled |
| Status | Modifier | Description |
|---|---|---|
| Member of Protected Users | ×0.2 | Strong Kerberos protections |
| Not a member | ×1.0 | No additional protection |
Protected Users membership is detected via:
- SID ending in
-525inmemberOf isProtectedUserflag in scoring context
// Cracking attacks: Kerberoast, AS-REP Roast
if (isCrackingAttack && hasSmartcardRequired) {
modifier *= 0.15; // Password is random, can't crack
}
// Delegation attacks: Unconstrained, Constrained, RBCD
if (isDelegationAttack && hasNotDelegated) {
modifier *= 0.3; // Delegation protection enabled
}
// All attacks: Account disabled
if (hasAccountDisable) {
modifier *= 0.1; // Account can't be used
}The correlation bonus increases the score when the same account appears in multiple risky findings. Non-admin accounts receive enhanced bonuses because multiple findings suggest potential privilege escalation paths.
correlationBonus = (numberOfFindings - 1) * 5
// Capped at 15 points| Findings | Bonus |
|---|---|
| 1 finding | +0 |
| 2 findings | +5 |
| 3 findings | +10 |
| 4+ findings | +15 (max) |
Rationale: When a non-admin user appears in 3+ findings, it often indicates an attack path exists (e.g., User → LAPS Read → Computer → gMSA → DCSync). These accounts deserve heightened attention.
// Non-admin accounts with 3+ findings get enhanced scoring
if (isNonAdmin && findingCount >= 3) {
correlationBonus = (numberOfFindings - 1) * 8
// Capped at 30 points
}| Findings | Non-Admin Bonus | Standard Bonus |
|---|---|---|
| 1 finding | +0 | +0 |
| 2 findings | +8 | +5 |
| 3 findings | +16 | +10 |
| 4 findings | +24 | +15 (max) |
| 5+ findings | +30 (max) | +15 (max) |
| Parameter | Standard | Non-Admin |
|---|---|---|
| Points per finding | 5 | 8 |
| Maximum bonus | 15 | 30 |
| Threshold | 2 | 3 |
Non-admin user john.doe appears in:
- LAPS Read Rights on OU=Servers
- Computer Owner of SRV-SQL01
- gMSA Password Reader for svc_backup$
- (svc_backup$ has DCSync rights)
Analysis:
- Account is non-admin (adminTier = 'none')
- Appears in 3 findings → triggers enhanced correlation
- Correlation bonus: (3-1) × 8 = +16 points
This elevated score reflects the potential attack path: john.doe → LAPS/Owner → Computer → gMSA → DCSync.
Certain finding combinations always receive maximum bonus:
| Combination | Bonus (Admin) | Bonus (Non-Admin) |
|---|---|---|
| DCSync + Kerberoast/ASREP | +15 | +30 |
| Delegation + Kerberoast | +10 | +16 |
Finding: Domain Admin with Kerberoastable SPN, old password, RC4 only
| Component | Value | Calculation |
|---|---|---|
| Base Score | 35 | Kerberoastable |
| Impact | ×2.0 | Tier 0 (Domain Admin) |
| Password Age | ×1.4 | 5× maxPwdAge |
| Encryption | ×1.3 | RC4 only |
| Policy Length | ×1.2 | 8-char minimum |
| Security | ×1.0 | No mitigations |
| Correlation | +10 | 3 findings |
Final: min(max((35 × 2.0 × 1.4 × 1.3 × 1.2 × 1.0) + 10, 0), 100) = 100 (capped)
Finding: Standard user, Kerberoastable, Smartcard Required, Protected Users
| Component | Value | Calculation |
|---|---|---|
| Base Score | 35 | Kerberoastable |
| Impact | ×1.0 | None (standard user) |
| Exploitability | ×1.0 | Recent password |
| Security (Smartcard) | ×0.15 | Password is random |
| Security (Protected) | ×0.2 | Protected Users member |
| Correlation | +0 | 1 finding |
Final: (35 × 1.0 × 1.0 × 0.15 × 0.2) + 0 = 1 (very low risk)
Finding: Non-admin user with DCSync rights
| Component | Value | Calculation |
|---|---|---|
| Base Score | 100 | DCSync |
| Impact | ×1.0 | Standard user (but irrelevant - DCSync IS compromise) |
| Exploitability | ×1.0 | N/A |
| Security | ×1.0 | No mitigations |
| Correlation | +0 | 1 finding |
Final: 100 × 1.0 × 1.0 × 1.0 + 0 = 100
The scoring context is built in Export-HTMLReport.ps1 via the Build-ScoringContext function:
$scoringContext = @{
accounts = @{} # Account info by sAMAccountName
findingContext = @{} # Finding metadata
correlations = @{} # Account appearance counts
domainInfo = @{
passwordPolicy = $null
maxPwdAgeDays = 0
minPwdLength = 0
complexityEnabled = $true
krbtgtLastReset = $null
}
}The scoring is performed client-side in the HTML report:
function calculateFindingScore(card) {
const baseScore = getBaseScore(title);
const impactMod = impactMultipliers[accountInfo.adminTier] || 1.0;
const exploitMod = getExploitabilityModifier(accountInfo, title);
const securityMod = getSecurityModifier(accountInfo, title);
const correlationBonus = getCorrelationBonus(accountInfo);
let finalScore = Math.round(baseScore * impactMod * exploitMod * securityMod) + correlationBonus;
return Math.min(Math.max(finalScore, 0), 100);
}Scores are displayed as badges with color coding:
| Score Range | Color | CSS Class |
|---|---|---|
| 80-100 | Red | score-critical |
| 60-79 | Orange | score-high |
| 40-59 | Yellow | score-medium |
| 20-39 | Blue | score-low |
| 0-19 | Gray | score-info |
All scoring values are centrally maintained in src/modules/Core/adPEAS-ScoringDefinitions.ps1.
Edit the $Script:FindingBaseScores hashtable:
$Script:FindingBaseScores = @{
# ... existing scores ...
'new finding keyword' = 45 # Add your keyword (lowercase)
}Edit the respective hashtables:
# Impact multipliers (Microsoft Tiering Model)
$Script:ImpactMultipliers = @{
'tier0' = 2.0 # Domain/Enterprise/Schema Admins (Domain Controllers)
'tier1' = 1.5 # Operators (Servers)
'tier2' = 1.2 # Other privileged (Workstations)
'none' = 1.0 # Standard accounts
}
# Security modifiers
$Script:SecurityModifiers = @{
'ACCOUNTDISABLE' = 0.1
'SMARTCARD_REQUIRED' = 0.15
# ...
}-
Password Age Estimation: Password age is calculated from
pwdLastSet, which may not reflect actual password strength. -
Encryption Types: The
msDS-SupportedEncryptionTypesattribute may not always reflect actual negotiated encryption. -
Protected Users Detection: Requires proper SID resolution; nested group membership may not be fully evaluated.
-
Policy Inheritance: Fine-grained password policies (PSOs) are not currently evaluated - only the default domain policy.
- Microsoft: Protected Users Security Group
- Microsoft: Kerberos Encryption Types
- SpecterOps: Kerberoasting
- HarmJ0y: Kerberos Delegation