================================================================================
  Collect-ADData.ps1
  LegacyMCP Offline Collector - Active Directory Data Export
  Version 1.7.0 - August 2026
  Marco Lelli, Impresoft 4ward
================================================================================

SYNOPSIS
--------
  Exports Active Directory configuration data to a structured JSON file
  for offline analysis with LegacyMCP.

  Read-only. No changes are made to the Active Directory environment.


DESCRIPTION
-----------
  Collect-ADData.ps1 collects a comprehensive inventory of an Active Directory
  forest or domain and exports it as a single JSON file. The JSON file is then
  loaded by the LegacyMCP MCP server for analysis with Claude.

  This offline workflow is designed for consultants working remotely or via
  desktop sharing: the script runs in the customer's environment, the JSON
  is exported and brought to the consultant's workstation for analysis.
  No network access to the customer environment is required during analysis.

  The script runs against a single domain. For multi-domain forests, run the
  script once per domain (or once with Enterprise Admin rights to collect
  forest-wide data automatically).

  Collected sections:
    - Forest info, functional levels, schema version, optional features
    - Schema extensions (custom attributes and classes)
    - Domains, default password policies
    - Domain Controllers, FSMO roles
    - EventLog configuration per DC (Application, System, Security)
    - NTP configuration per DC (server, type, advanced W32Time registry keys)
    - SYSVOL replication state (DFSR) per DC
    - Sites and site links
    - Users (with UPN, DN, mail, adminCount, last logon, password info)
    - Privileged accounts (Domain Admins, Enterprise Admins, Schema Admins, etc.)
    - Groups (with DN, adminCount, member count)
    - Privileged group memberships (recursive)
    - Organizational Units, blocked inheritance, GPO links
    - GPO inventory and GPO links
    - Trust relationships
    - Fine-Grained Password Policies
    - DNS zones and forwarders
    - Computer objects (OS, last logon, CNO/VCO detection)
    - PKI / Certification Authority discovery

  DC reachability: if a Domain Controller cannot be contacted for registry-based
  queries (NTP, EventLog), the script records the failure and continues. The
  output JSON marks unreachable DCs explicitly so the analyst knows which data
  is complete and which is partial.


REQUIREMENTS
------------

  PowerShell version:
    Minimum: PowerShell 5.1 (Windows Management Framework 5.1)
    Recommended: PowerShell 5.1 or 7.x

  Modules required:
    ActiveDirectory  -- included in RSAT or available on Domain Controllers
    GroupPolicy      -- required for GPO inventory (Get-GPO, Get-GPInheritance)
                       included in RSAT Group Policy Management Tools

  RSAT installation (Windows 10 / Windows 11):
    Add-WindowsCapability -Online -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0
    Add-WindowsCapability -Online -Name Rsat.GroupPolicy.Management.Tools~~~~0.0.1.0

  The script does NOT need to run on a Domain Controller.
  It can run from any domain-joined workstation with RSAT installed.

  Elevated session: the script should be run from an elevated PowerShell session
  (Run as Administrator) to ensure remote registry access for NTP and EventLog
  configuration queries.

  This also matters for user and computer data: in some AD environments, a
  non-elevated session causes userAccountControl to come back unreadable for
  every account, which silently empties Enabled, PasswordNeverExpires, and
  the delegation fields (TrustedForDelegation, TrustedToAuthForDelegation) --
  see the "users"/"computers" field notes in OUTPUT FORMAT below and the
  v1.7.0 entry in VERSION HISTORY. Since v1.7.0 the script detects this and
  prints a non-blocking warning; it does not stop the collection.


REQUIRED RIGHTS
---------------

  Minimum -- POLP baseline (recommended):
    A delegated account (or gMSA) with the minimum set of permissions
    documented in docs/minimum-permissions.md. The matrix is certified
    in the field at 21/22 PASS (T18 fails by design on Windows Server
    2012 R2). Apply it with Set-LegacyMCPPermissions.ps1 and verify
    with Test-LegacyMCPPermissions.ps1 (both in installer/).

    Note: NTP and EventLog registry queries and some less common ACLs
    are covered by the delegated permissions in the matrix; if any
    delegation is missing, the affected sections return incomplete
    data rather than failing the whole collection.

  Fallback -- full forest inventory or when delegation is not practical:
    Domain Admin, or Enterprise Admin for a full forest inventory
    (multiple domains, forest-level optional features, cross-domain
    privileged group memberships, trust relationships between domains
    in different trees). Not the minimum requirement -- use only when
    the POLP baseline cannot be applied.

  gMSA (Group Managed Service Account):
    If running LegacyMCP as a Windows service in Live Mode, a gMSA is the
    recommended account type. The gMSA needs the POLP baseline above (or
    Domain Admin as fallback) on each domain in scope.


PARAMETERS
----------

  -OutputPath <string>
      Path to the output JSON file.
      Default: .\<forest>_ad-data.json  (e.g. contoso.local_ad-data.json)
      The forest name is resolved at runtime from the target environment.

      BREAKING CHANGE from v1.4: the default filename is now <forest>_ad-data.json
      instead of ad-data.json. Callers using -OutputPath explicitly are not affected.

      A companion log file is written to the same directory with the same stem
      and a .log extension (e.g. contoso.local_ad-data.log). The log contains
      one timestamped entry per section with counts, and per-section duration
      timings when -Verbose is active.

      If the file already exists, it is renamed with a timestamp suffix
      (e.g. contoso.local_ad-data_backup_20260329_143022.json) before the new
      export is written. The original data is never silently overwritten.
      Use a dedicated folder to keep exports organized by customer and date.

  -Server <string>
      FQDN or NetBIOS name of the Domain Controller to query.
      If omitted, PowerShell auto-discovers the closest DC for the current
      user's domain.

      Use this parameter to target a specific DC, or when running the script
      from a workstation not joined to the target domain (e.g., over VPN
      with explicit DC targeting).

  -Credential <PSCredential>
      Credentials to use for all AD queries.
      If omitted, the script uses the current user context (recommended when
      running as a domain user with appropriate rights, or as a gMSA).

      To build a credential object interactively:
          $cred = Get-Credential


EXAMPLES
--------

  --- Basic usage: current user, auto DC discovery ---

      .\Collect-ADData.ps1

      Runs against the current user's domain. Output saved to
      .\<forest>_ad-data.json (e.g. contoso.local_ad-data.json).
      Log saved alongside as contoso.local_ad-data.log.


  --- Basic usage with verbose timing ---

      .\Collect-ADData.ps1 -Verbose

      Same as above, but adds per-section duration timings to both the console
      and the log file. Useful for diagnosing slow sections (e.g. group_members
      in large environments).


  --- Specify output path ---

      .\Collect-ADData.ps1 -OutputPath C:\Export\contoso-$(Get-Date -Format yyyyMMdd).json

      Saves the output to C:\Export\ with a date-stamped filename.
      Recommended for keeping multiple exports organized.


  --- Target a specific Domain Controller ---

      .\Collect-ADData.ps1 -Server dc01.contoso.local -OutputPath C:\Export\contoso.json

      Useful when auto-discovery selects an unexpected DC, or when collecting
      data from a specific site's DC.


  --- Alternate credentials (single domain) ---

      $cred = Get-Credential contoso\svc-legacymcp
      .\Collect-ADData.ps1 -Credential $cred -OutputPath C:\Export\contoso.json

      Use when running from a workstation not joined to the target domain,
      or when the current user does not have the required rights.


  --- Full forest collection (Enterprise Admin) ---

      $cred = Get-Credential contoso\enterprise-admin
      .\Collect-ADData.ps1 -Server dc01.contoso.local `
          -Credential $cred `
          -OutputPath C:\Export\contoso-forest-$(Get-Date -Format yyyyMMdd).json

      Collects forest-wide data including all domains, cross-domain trust
      relationships, and forest-level optional features.
      Recommended for full AD assessments.


  --- Child domain only (no Enterprise Admin) ---

      $cred = Get-Credential child\domain-admin
      .\Collect-ADData.ps1 -Server dc01.child.contoso.local `
          -Credential $cred `
          -OutputPath C:\Export\child-domain.json

      Scope is limited to the child domain. Forest-level sections (forest
      optional features, cross-forest trusts) will reflect what is visible
      from the child domain's context.


  --- Multi-domain forest: one export per domain ---

      # Forest root
      .\Collect-ADData.ps1 -Server dc01.contoso.local `
          -OutputPath C:\Export\contoso-root.json

      # Child domain
      .\Collect-ADData.ps1 -Server dc01.child.contoso.local `
          -OutputPath C:\Export\contoso-child.json

      Load both JSON files into LegacyMCP with multi-scope workspace
      configuration for cross-domain analysis.


  --- Impresoft 4ward: standard assessment export ---

      $customer = "ClienteXYZ"
      $date     = Get-Date -Format "yyyyMMdd"
      $cred     = Get-Credential

      .\Collect-ADData.ps1 `
          -Server dc01.clientexyz.local `
          -Credential $cred `
          -OutputPath "C:\Impresoft\Assessments\$customer\ad-data-$date.json"

      Recommended naming convention for multi-customer environments.


DATA STORAGE
------------

  Store JSON output files in a dedicated folder OUTSIDE the repository.
  AD data is sensitive -- it must never be committed to GitHub.

  Recommended base path:   C:\LegacyMCP-Data\

  Naming convention:
    <domainname>-data.json                    plain export
    <domainname>-data-<yyyyMMdd>.json         date-stamped (recommended)

  Examples:
    C:\LegacyMCP-Data\contoso.local-data-20250317.json
    C:\LegacyMCP-Data\child.contoso.local-data-20250317.json
    C:\LegacyMCP-Data\fabrikam.local-data-20250317.json

  WHY THIS MATTERS
  The JSON file contains the full AD inventory: user accounts, group
  memberships, password policies, trust relationships, PKI topology.
  This is exactly the data an attacker needs to map an environment.

  Rules:
    - Never save JSON files inside the repository working directory.
    - Never commit JSON files to git (add *.json to .gitignore if needed).
    - Never send JSON files via unencrypted channels (email, Teams chat).
    - Treat JSON files with the same classification as the customer's
      Active Directory backup -- typically Confidential or Restricted.
    - Delete files when no longer needed for the assessment.

  The repository already includes C:\LegacyMCP-Data\ in .gitignore.
  If using a custom path, add it manually.


OUTPUT FORMAT
-------------

  The script produces a single UTF-8 JSON file.

  Top-level keys in the JSON:
    forest, optional_features, schema, domains, default_password_policy,
    dcs, fsmo_roles, eventlog_config, ntp_config, sysvol, sites, site_links,
    users, privileged_accounts, groups, privileged_groups, group_members,
    ous, gpos, gpo_links, blocked_inheritance, trusts, fgpp, dns,
    dns_forwarders, computers, pki

  Sections that fail entirely (e.g., GPO cmdlets not available) are recorded
  as null with a warning printed to the console. The JSON remains valid and
  loadable by LegacyMCP -- missing sections are handled gracefully.

  Typical file size:
    Small domain  (< 500 objects):   1-5 MB
    Medium domain (500-5000 objects): 5-30 MB
    Large domain  (> 5000 objects):  30 MB+

  Users and computers have no collection limit by default (since v1.7.0).
  A -Limit parameter exists for test/debug use only -- it is opt-in and
  does not apply during a normal assessment. This does not apply to every
  section: schema extensions keep a default cap -- see "schema" below.

  Fields per section (selected):

  schema
    lDAPDisplayName, ObjectClass, AdminDescription, GovernsID, AttributeID
    Capped at 500 custom schema extensions by default (since v1.6.4) --
    few environments have more than a few dozen, so this is a safety net,
    not a silent data-loss risk. Since v1.7.0 the cap is configurable via
    -Limit (0 = unlimited, for the rare environment that needs a full
    collection) and a warning is logged through the same Write-SafeCollectorLog
    path used elsewhere when the cap actually truncates the result (task
    #130). Live Mode applies the identical 500 cap to the "schema" section,
    with no -Limit override available there -- use the offline collector
    with -Limit 0 if a complete list is needed for an environment that
    exceeds 500 custom extensions.

  users
    SamAccountName, DisplayName, UserPrincipalName, DistinguishedName,
    Mail, Enabled, PasswordNeverExpires, LockedOut, LastLogonDate,
    PasswordLastSet, Description, AdminCount,
    TrustedForDelegation, TrustedToAuthForDelegation, AllowedToDelegateTo
    Since v1.7.0: Enabled, PasswordNeverExpires, TrustedForDelegation,
    TrustedToAuthForDelegation, and PasswordNotRequired are explicit null
    -- instead of a fabricated value -- for any user whose
    userAccountControl could not be read (see "Elevated session" above).
    All five fields come from the same read and go null together for the
    same user. An aggregate warning is printed when this happens.

  computers
    Name, DistinguishedName, OperatingSystem, OperatingSystemVersion,
    Enabled, LastLogonDate, PasswordLastSet, Description,
    IsCNO, IsVCO,
    TrustedForDelegation, TrustedToAuthForDelegation, AllowedToDelegateTo
    Computer accounts are not affected by the userAccountControl read
    failure described under "users" above.

  group_members
    GroupName, MemberSamAccountName, MemberDisplayName,
    MemberObjectClass, MemberDistinguishedName, MemberEnabled
    One row per direct member per group. MemberEnabled is null for
    nested group members (objectClass = group). Groups with no members
    produce no rows.
    Since v1.7.0: a member whose DN cannot be resolved (orphaned SID,
    deleted object, unresolvable external principal) no longer drops the
    whole group from the export. It is recorded as a placeholder row with
    MemberObjectClass = "unresolved" -- MemberDistinguishedName still
    contains the raw DN, which is the starting point for cleanup.

    The same member-by-member resolution (task #134) also protects
    privileged_groups (placeholder entries with lowercase objectClass =
    "unresolved" -- different field names than above, same idea) and the
    groups section (MemberCount now counts raw member DNs directly instead
    of failing, so it stays accurate even with unresolvable members inside).
    privileged_accounts is the one exception: unresolvable members are
    excluded from it on purpose, to keep its count accurate -- they remain
    visible in privileged_groups and group_members instead.

  gpo_links
    DisplayName, GpoId, Enabled, Enforced, Target, Order
    One row per GPO link per target container (domain root or OU).
    The same GPO linked to multiple OUs appears as multiple rows,
    each with its own Target field.

  eventlog_config (per DC, per log)
    DC, LogName, MaxSizeBytes, OverflowAction
    Note: OverflowAction maps to LogMode values: Circular, Retain,
    AutoBackup. There is no RetentionDays equivalent in the
    Get-WinEvent API.


MULTI-FOREST CONFIGURATION
--------------------------

  When the assessment scope spans multiple forests or multiple standalone
  domains, load all JSON files into LegacyMCP via the workspace section
  of config.yaml. The MCP server merges them into a single SQLite database,
  tagging every object with its source domain for cross-domain queries.

  --- Scenario A: multiple standalone forests (independent customers or BUs) ---

  Each forest is treated as an independent scope. LegacyMCP loads all files
  but does not infer any relationship between them.

      workspace:
        forests:
          - name: contoso.local
            relation: standalone
            module: ad-core
            file: C:\LegacyMCP-Data\contoso.local-data-20250317.json
          - name: fabrikam.local
            relation: standalone
            module: ad-core
            file: C:\LegacyMCP-Data\fabrikam.local-data-20250317.json
          - name: tailspin.local
            relation: standalone
            module: ad-core
            file: C:\LegacyMCP-Data\tailspin.local-data-20250317.json

  Use this when auditing multiple unrelated environments in a single session.
  Claude can compare them but LegacyMCP will not generate migration findings.


  --- Scenario B: migration -- source forest and destination forest ---

  Mark one forest as source and one as destination. LegacyMCP enables
  migration-specific queries: objects present in source but not in destination,
  naming conflicts, SIDHistory mapping for already-migrated accounts.

      workspace:
        forests:
          - name: contoso.local
            relation: source
            module: ad-core
            file: C:\LegacyMCP-Data\contoso.local-data-20250317.json
          - name: corp.fabrikam.com
            relation: dest
            module: ad-core
            file: C:\LegacyMCP-Data\corp.fabrikam.com-data-20250317.json

  Mark one forest as relation: source and one as relation: dest.
  LegacyMCP surfaces comparative findings:
    - Users in source without a match in destination
    - Groups with no equivalent in the target
    - SIDHistory entries for already-migrated principals
    - UPN / SAMAccountName conflicts between the two environments


  --- Scenario C: multi-domain forest (one export per domain) ---

  Run the collector once per domain (see EXAMPLES above) and list all
  resulting files under the same forest entry. Use this when Enterprise
  Admin rights were not available and each domain was collected separately.

      workspace:
        forests:
          - name: contoso.local
            relation: standalone
            module: ad-core
            file: C:\LegacyMCP-Data\contoso.local-data-20250317.json
          - name: child.contoso.local
            relation: standalone
            module: ad-core
            file: C:\LegacyMCP-Data\child.contoso.local-data-20250317.json
          - name: eu.contoso.local
            relation: standalone
            module: ad-core
            file: C:\LegacyMCP-Data\eu.contoso.local-data-20250317.json


NOTES
-----

  - NTP and EventLog registry queries connect to each DC individually.
    DCs that are unreachable or have remote registry disabled will be marked
    with null values and a Status field set to "Unreachable" in the output.
    Collection continues on remaining DCs.

  - SYSVOL replication state is queried via WMI (root\MicrosoftDFS).
    Environments still using FRS (File Replication Service) instead of DFSR
    will return "Unknown" for SYSVOL state. FRS migration to DFSR is strongly
    recommended on any domain functional level below Windows Server 2008 R2.

  - GPO sections require the GroupPolicy module. If the module is not
    available, GPO inventory is skipped with a warning. Install RSAT Group
    Policy Management Tools to enable this section.

  - The script does NOT collect DHCP, PKI configuration, or GPO content
    analysis. These are covered by the LegacyMCP Enterprise layer.

  - The JSON output file may contain sensitive information (user accounts,
    group memberships, password policy settings). Handle it according to
    your organization's data classification policy and the customer's
    confidentiality requirements.

  - The script file is ASCII-only. No extended UTF-8 characters (em dashes,
    curly quotes, or any codepoint above U+007F) are used in the source.
    Reason: PowerShell on Windows reads files without a UTF-8 BOM using the
    system ANSI code page (CP1252). Multi-byte UTF-8 sequences are
    misinterpreted as separate characters, which can corrupt string literals
    and cause hard-to-diagnose parse errors. Keep the file ASCII-only when
    editing.


VERSION HISTORY
---------------

  v1.7.0 - August 2026
    - Collect-ADData.ps1: -Limit default changed from a silent cap
      (5,000 users / 10,000 computers) to unlimited (0). The cap is
      now opt-in only, for test/debug use.
    - Collect-ADData.ps1: forest and domain name resolution anchored
      explicitly to the target server (-Server) instead of the calling
      machine's own identity. Fixes wrong output filenames and an
      entirely wrong "forest" section / fsmo_roles in cross-forest
      scenarios (multi-domain migrations, trust relationships).
    - Collect-ADData.ps1: added a non-blocking elevation check. A
      warning is shown -- and recorded in the log and in
      _metadata.elevated -- when the session is not running as
      Administrator. Some AD environments silently return a null
      userAccountControl without elevation, which affects Enabled,
      PasswordNeverExpires, and the delegation fields below.
    - Users.psm1: Enabled, PasswordNeverExpires, TrustedForDelegation,
      TrustedToAuthForDelegation now report explicit null (with an
      aggregate warning) instead of a fabricated value when
      userAccountControl cannot be read for a user. PasswordNotRequired
      was fixed identically in a follow-up pass (task #135) -- same root
      cause, same aggregate warning, no separate opt-in needed.
    - Schema.psm1: schema extensions collection is now capped via a
      -Limit parameter (default 500, 0 = unlimited) instead of a fixed
      500 with no override. A warning is logged through
      Write-SafeCollectorLog when the cap actually truncates the result.
      Live Mode applies the same 500 cap to its "schema" section, with no
      -Limit override on that side (task #130).
    - Forest.psm1: FSMO role data resolved from the target domain
      instead of the calling machine's own forest.
    - New Membership.psm1 module: group membership is now resolved
      member-by-member instead of failing an entire group when a
      single member cannot be resolved (previously affected
      Administrators, Backup Operators, and other groups with even
      one broken reference). Applies to Groups, Privileged Groups,
      Privileged Accounts, and Group Members sections. Unresolvable
      members are recorded as placeholder rows
      (MemberObjectClass = "unresolved") instead of disappearing.
    - New Logging.psm1 module: shared fallback logging path
      (Write-SafeCollectorLog) so warnings from shared modules are
      never silently lost regardless of how the collector is invoked.
    - DomainControllers.psm1, Groups.psm1: large collections (DCs,
      groups) are now fully read before processing, avoiding an AD
      Web Services timeout on environments with very large group
      counts.

  v1.6.5 - May 2026
    - Collect-ADData.ps1: pki section now always serialized as JSON array.
      @() wrapping added at the assignment site so ConvertTo-Json receives
      an array even when a single CA is present (P2, P8).
    - PKI.psm1: pipeline output wrapped in @() for defensive consistency.
    - live.py: pki embedded PS command wrapped in @() before ConvertTo-Json
      to align Live Mode behavior with collector (P2).

  v1.6.4 - May 2026
    - DNS.psm1: iterate all DCs for zone collection instead of first DC only; warn if no DC has DNS role
    - PKI.psm1: tighten CA filter to objectClass pKIEnrollmentService only
    - Schema.psm1: warn when schema extensions exceed 500 and are truncated
      (fixed cap, no override -- superseded by the -Limit-based mechanism
      in v1.7.0 above)
    - Groups.psm1: warn on group member enumeration failure instead of silent skip
    - FSP.psm1: warn on FSP collection failure instead of silent empty return
    - Fixed em dash characters in module headers (ASCII compliance, P11)

  v1.6.3 - April 2026
    - Refactored all inline sections: moved 8 remaining inline data collection
      blocks from Collect-ADData.ps1 into their respective .psm1 modules.
      No logic changes -- all behavior is identical to the certified inline
      implementations. Changes per module:
      Schema.psm1: Get-SchemaExtensionsData updated with OID-based filtering,
        correct field set (governsID, attributeID), limit raised to 500.
      Groups.psm1: Get-GroupsData updated with Get-ADGroupMember | Measure-Object
        for correct count on large groups (>1500 members). Get-GroupMembersData
        added for the group_members flat table.
      GPO.psm1: Get-GPOLinksData updated to iterate all OUs (not just domain root).
      Computers.psm1: Get-ComputersData updated with TrustedForDelegation,
        TrustedToAuthForDelegation, AllowedToDelegateTo (msDS-AllowedToDelegateTo).
      OUs.psm1: Get-BlockedInheritanceData added for blocked_inheritance section.
      Forest.psm1: Get-FSMOForestData added (SchemaMaster, DomainNamingMaster).
      Domains.psm1: Get-FSMODomainData added (PDCEmulator, RIDMaster,
        InfrastructureMaster) -- merged with forest roles into fsmo_roles.
      FSP.psm1: new module with Get-FSPData for Foreign Security Principals.

  v1.6.2 - April 2026
    - Fix msDFSR-Flags mapping in DomainControllers.psm1: map integer values
      (0/16/32/48) to human-readable strings (Start/Prepared/Redirected/
      Eliminated). Previously the collector emitted "Not Configured" even when
      the attribute was present and had a valid value.

  v1.6.1 - April 2026
    - DC Inventory reporting: found/collected/failed counts added to
      Get-DCFeaturesData, Get-DCServicesData, Get-DCInstalledSoftwareData
      in DomainControllers.psm1.
    - Webster gap closure: NTP advanced W32Time registry keys, EventLog
      configuration per DC, AD file locations (NTDS, SYSVOL, log paths),
      schema fingerprints, Foreign Security Principals, DC network
      configuration (IP addresses, DNS servers), TombstoneLifetime with
      fallback to AD default (180 days) when attribute not set.

  v1.6 - April 2026
    - DC Inventory: three new collector sections per Domain Controller --
      dc_windows_features (installed Windows Server roles), dc_services
      (running and auto-start services), dc_installed_software (installed
      software from registry). Implemented in DomainControllers.psm1.
    - SIDHistory field added to Users section; filterable via
      has_sid_history in get_users.
    - Backward compatible: LegacyMCP servers older than v1.6 handle missing
      sections gracefully.

  v1.5 - March 2026
    - BREAKING CHANGE: default output filename is now <forest>_ad-data.json
      (e.g. contoso.local_ad-data.json) instead of ad-data.json. Callers
      using -OutputPath explicitly are not affected.
    - Added companion log file: written alongside the JSON with the same stem
      and a .log extension. Contains one timestamped entry per section with
      counts. With -Verbose, includes per-section duration timings.
    - Added Write-CollectorLog function: uniform logging to file and console
      with levels INFO (green), WARN, ERROR (non-terminating), VERBOSE.
      Increments session counters $script:sectionsOK, sectionsWarn,
      sectionsError.
    - Added session header and footer to the log file: forest name, DC,
      output and log paths, start/end timestamps, duration, and section
      summary (OK/Warn/Error counts).
    - Added collection_summary to _metadata in the output JSON: fields
      sections_ok, sections_warn, sections_error, log_file. Allows
      LegacyMCP to surface collection completeness in tool responses.
    - Per-section duration measurement via System.Diagnostics.Stopwatch.
    - Confirmed [CmdletBinding()] present -- -Verbose works as a native switch.

  v1.4 - March 2026
    - Added _metadata block as the first key of the output JSON. Fields:
      module ("ad-core"), version ("1.0"), forest (forest name), collected_at
      (UTC ISO 8601), collector_version ("1.4"), collected_by (DOMAIN\username).
    - Format is identical to the _metadata produced by LegacyMCP Live Mode
      snapshots, enabling interoperability between offline and live workflows.
    - Fixed silent failure in export section: all four export blocks (pre-check,
      build metadata, write file, post-check) are now wrapped in explicit
      try/catch with Write-Status reporting and descriptive throw messages.
      Previously a failure in any block would terminate the script without
      a clear error message due to $ErrorActionPreference = "Stop".

  v1.3 - March 2026
    - Pre-check: if the output file already exists, it is renamed with a
      timestamp suffix (e.g. ad-data_20260327_143000.json) before the new
      export is written. Prevents silent overwrite of previous exports.
    - Export uses -NoClobber to enforce that the rename happened correctly.
    - Post-check: after writing, the output file is read back and parsed as
      JSON to verify integrity. If validation fails, the corrupt file is
      renamed to *_corrupt.json and an exception is thrown.

  v1.2 - March 2025
    - Added group_members section: flat table with one row per direct
      member per group. Fields: GroupName, MemberSamAccountName,
      MemberDisplayName, MemberObjectClass, MemberDistinguishedName,
      MemberEnabled. MemberEnabled is resolved via Get-ADUser /
      Get-ADComputer and is null for nested group members. Handles
      LDAP range retrieval for large groups via Get-ADGroupMember.
    - Fixed gpo_links collection: now iterates all OUs with
      Get-GPInheritance instead of domain root only. Returns all GPO
      links across the entire domain, one row per link per target OU.
      Previously only links on the domain root were collected.

  v1.1 - March 2025
    - Fixed EventLog collection: removed RetentionDays field (property
      does not exist on EventLogConfiguration objects). OverflowAction
      now correctly maps to LogMode (Circular/Retain/AutoBackup).
    - Fixed schema extensions filter: OID-based exclusion of Microsoft
      base schema and Exchange objects. governsID and attributeID added
      to output. Limit raised from 200 to 500.
    - Fixed MemberCount for large groups: replaced $_.Members.Count with
      Get-ADGroupMember, which handles LDAP range retrieval transparently.
      Groups with membership exceeding MaxPageSize (~1500) now return the
      correct count instead of 0.
    - Added Kerberos delegation fields to users and computers:
      TrustedForDelegation, TrustedToAuthForDelegation,
      AllowedToDelegateTo (msDS-AllowedToDelegateTo).

  v1.0 - March 2025
    Initial release.


================================================================================
  LegacyMCP -- https://github.com/Marco-Lelli/legacy-mcp
  Impresoft 4ward -- https://www.4ward.it/
  Legacy Things -- https://legacythings.it
================================================================================
