Source: netwrix/pingcastle @ d0f3425

How It Works

PingCastle is an Active Directory security assessment tool that evaluates domain configurations against ~200 built-in risk rules, producing a scored HTML report. The philosophy is “80% of security findings in 20% of the time” — it doesn’t enumerate everything about a domain but instead focuses on the highest-impact misconfigurations: stale accounts, trust issues, privileged group weaknesses, and anomalies.

The tool operates in several modes: healthcheck (the primary AD assessment), cartography (multi-domain trust mapping), Entra ID (cloud) assessment, individual scanners (ZeroLogon, MS17-010, BlueKeep, spooler, etc.), and data export. Each mode follows the same pattern: connect to AD via ADWS/LDAP/RPC, collect data into a typed data model, run rules against it, score the results, and generate an HTML report with embedded JavaScript for interactivity.

PingCastle also supports a commercial/agent deployment model. It can retrieve licenses and configuration from an API endpoint, upload results centrally, and restrict scanning to licensed domains — the open-source tier is free for internal use but has some feature limitations (e.g., honeypot data is capped in the basic edition).

Architecture

The solution has three projects:

ProjectFrameworkPurpose
PingCastlenet8.0-windowsMain executable — AD interaction, healthcheck engine, scanners, reports
PingCastleCommonnet8.0Shared library — data models, rule framework, cloud/Graph abstractions, templates
PingCastleAutoUpdater—Agent-side auto-update utility

Key directories in the main project:

  • ADWS/ — AD connection layer (ADWS via WCF, LDAP, domain locator)
  • RPC/ — Hand-rolled RPC clients for NRPC, LSARPC, SAMR, DCOM, Spooler (MIDL format strings for x86/x64)
  • Healthcheck/ — The HealthcheckAnalyzer — the 6700-line core that collects all AD data
  • Scanners/ — 17 standalone vulnerability scanners
  • Report/ — HTML report generators (map builder, network/Hilbert map)
  • Cloud/ — Entra ID assessment via MS Graph API
  • Graph/ — Compromise graph construction and reporting

The PingCastleCommon library holds the data models (HealthcheckData, HealthCheckCloudData), the rule engine (RuleSet<T>, RuleBase<T>), and ~211 rule implementations across AD and Cloud subdirectories.

The Spine

Entry point: Program.Main() initializes DI, loads licenses, parses command-line arguments, and dispatches to Tasks.

Program.Main(args)
  → InitializeServiceProvider()          // DI + factory registry
  → License validation
  → ParseCommandLine(args)              // maps flags → PossibleTasks
  → actions[action].Invoke()            // delegates to Tasks methods
  → Tasks.CompleteTasks()               // email upload, API submission

Healthcheck flow (the primary path):

Tasks.AnalysisTask<T>()
  → HealthcheckAnalyzer.PerformAnalyze(parameters)
    → ADWebService(server, port, credential)    // connects via ADWS or LDAP
    → GenerateGeneralData()                     // domain info, schema, forest
    → GenerateUserData()                        // users, password stats
    → GenerateComputerData()                    // machines, OS breakdown
    → GenerateTrustData()                       // trust relationships
    → GeneratePrivilegedGroupAndPermissionsData()
    → GenerateDelegationData()                  // delegation/GPO
    → GeneratePKIData()                         // certificate authorities
    → GenerateDomainControllerData()            // DCs + null session + RPC
    → ... (exchange, DNS, WSUS, anomalies)
    → RuleSet<HealthcheckData>.ComputeRiskRules(data)  // run all 188+ rules
    → scores accumulated into GlobalScore, StaleObjectsScore, etc.

Scanner flow is simpler: Tasks.ScannerTask() → PingCastleFactory.LoadScanner(type) → IScanner.Initialize() → IScanner.Export().

Cloud flow: Tasks.AnalyzeTask() → Cloud.Analyzer → MS Graph API calls → RuleSet<HealthCheckCloudData>.ComputeRiskRules().

Key Patterns

Attribute-driven rule system. Each rule is a class decorated with attributes that declare its risk model, scoring, maturity level, and compliance framework mappings:

[RuleModel("A-AdminSDHolder", RiskRuleCategory.Anomalies, RiskModelCategory.TemporaryAdmins)]
[RuleComputation(RuleComputationType.TriggerOnThreshold, 50, Threshold: 50, Order: 1)]
[RuleComputation(RuleComputationType.TriggerOnThreshold, 45, Threshold: 45, Order: 2)]
[RuleANSSI("R40", "paragraph.3.6.3.1")]
[RuleMitreAttackMitigation(MitreAttackMitigation.PrivilegedAccountManagement)]
public class HeatlcheckRuleAnomalyAdminSDHolder : RuleBase<HealthcheckData>
{
    protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { ... }
}

Scoring uses RuleComputationType — TriggerOnThreshold, TriggerOnPresence, PerDiscover, TriggerIfLessThan — allowing graduated point assignments based on finding severity/count. Rules also carry STIG, ANSSI, DUR-ANSSI, and MITRE ATT&CK cross-references as attributes.

Multi-protocol AD access. ADWebService tries ADWS (WCF/NetTcp) first on Windows, falls back to LDAP. On Unix it uses LDAP only. The IADConnection interface abstracts over ADWSConnection, LDAPConnection, and WindowsFileConnection. RPC protocols (NRPC, LSARPC, SAMR, DCOM, Spooler) are implemented with hand-written MIDL stubs.

Data collection then analysis separation. The HealthcheckAnalyzer populates a HealthcheckData object (a 2400-line POCO with ~200 properties) entirely before any rules run. This means rules are pure functions of the data model — they never touch AD directly.

Plugin discovery via assembly scanning. PingCastleFactoryRegistry registers assemblies, and PingCastleFactory scans them for IScanner and IExport implementations. Rules are discovered similarly via RuleSet<T> which reflects over the assembly for RuleBase<T> subclasses.

Template-based HTML reports. Reports use embedded gzipped CSS/JS (Bootstrap, vis.js, jQuery) with a responsive template. The TemplateManager handles decompression. Reports include interactive network maps, Hilbert space-filling curve visualizations, and risk control matrices.

Non-Obvious Details

RPC is hand-rolled. The PingCastle/RPC/ directory contains raw MIDL format strings for x86 and x64 architectures, implementing NRPC, SAMR, LSARPC, and Spooler protocols directly. This avoids external dependencies but means the code handles platform-specific RPC marshalling manually.

License is embedded as a base64 string. The "basic edition" license is compiled into Program.cs as a constant (BasicEditionLicense). This allows the tool to run out-of-the-box without configuration, with commercial features gated by a license loaded from a .config file or retrieved via API.

Thread.Abort is used in carto mode. The cartography feature spawns 100 threads with a BlockingQueue and calls Thread.Abort() in the finally block for cleanup. This is a deprecated .NET pattern and can cause resource corruption.

Honeypot accounts are limited in the basic edition. LimitHoneyPot = true in the basic license restricts detection of decoy accounts — a deliberate feature gate for the open-source tier.

Unix support exists but is constrained. On Unix platforms, PingCastle falls back to LDAP-only connections (no ADWS) and skips null session and RPC-based checks entirely. The main executable targets net8.0-windows specifically.

HealthcheckAnalyzer is 6700 lines. This single file contains all AD data collection logic. While well-organized with clearly named Generate* methods, it's a god-class that would benefit from decomposition.

Framework cross-references are deep. Rules carry STIG, ANSSI DUR-ANSSI, MITRE ATT&CK, and CERT-FR references as attributes. The RuleAttribute.cs file alone is 589 lines just defining these cross-reference types.

Assessment

Strengths:

  • Mature rule engine. 211 rules with graduated scoring, compliance framework mapping, and a clean attribute-driven model. Adding a new rule is a single class file with attributes.
  • Multi-protocol coverage. ADWS, LDAP, and raw RPC protocols (NRPC, SAMR, LSARPC, DCOM, Spooler) give it access to data layers that LDAP-only tools can’t reach.
  • Practical output. HTML reports with interactive visualizations are immediately useful for non-technical stakeholders. XML/JSON export enables pipeline integration.
  • Dual cloud/on-prem. The same architecture handles both Active Directory and Entra ID assessments, sharing the rule framework.
  • Well-structured DI migration. The codebase is mid-way through adopting Microsoft.Extensions.DependencyInjection — service registration exists alongside manual instantiation with fallback paths.

Concerns:

  • God-class analyzer. HealthcheckAnalyzer.cs at 6700 lines is difficult to navigate and test in isolation. The data collection methods should be extracted into focused classes.
  • Windows-only primary path. The main executable targets net8.0-windows with Windows Forms references. While PingCastleCommon is cross-platform, the primary use case requires Windows.
  • Thread safety in carto. Using Thread.Abort() and a 100-thread pool for multi-domain scanning is fragile. Modern Task/async patterns would be safer and more efficient.
  • License coupling. License checks are interleaved throughout the execution path, and the embedded base64 license creates an awkward trust boundary for security-conscious users.
  • No test project visible. The repository lacks a test suite, which is concerning for a security assessment tool where correctness directly impacts trustworthiness of results.

Recommendations for contributors:

  • New rules: extend RuleBase<HealthcheckData> in PingCastleCommon/Healthcheck/Rules/, use the [RuleModel] and [RuleComputation] attributes. Follow existing naming convention (HeatlcheckRule{Category}{Finding}).
  • New scanners: implement IScanner (or extend ScannerBase) in PingCastle/Scanners/. They’re auto-discovered via assembly scanning.
  • Report changes: modify templates in PingCastleCommon/Template/ and generators in PingCastle/Report/.
  • analyzing-sharphound — Another AD security tool; compares PingCastle’s rule-based scoring approach to BloodHound’s graph-based attack path analysis.