Initial release: Declarative AD Framework v2.1.0
Infrastructure-as-code framework for Active Directory objects and Group Policy. Sanitized from production deployment for public sharing.
This commit is contained in:
commit
f172d00514
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Temporary credential files (ACL-locked, contain plaintext passwords)
|
||||||
|
ad-objects/.credentials/
|
||||||
|
|
||||||
|
# Compiled DSC MOF files
|
||||||
|
gpo/Output/
|
||||||
|
|
||||||
|
# GPO backups (local snapshots, not committed)
|
||||||
|
gpo/backups/
|
||||||
|
|
||||||
|
# JetBrains IDE
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
.claude/
|
||||||
173
CHANGELOG.md
Normal file
173
CHANGELOG.md
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [2.1.0] - 2026-02-14
|
||||||
|
|
||||||
|
AD objects subsystem refactored into modular architecture matching the GPO pattern. New features for password policies, user properties, group protection, and stale object detection.
|
||||||
|
|
||||||
|
### AD Objects -- Modular Architecture
|
||||||
|
|
||||||
|
Refactored `ADHelper.ps1` from a monolithic library (606 lines) into a loader that dot-sources 6 specialized modules:
|
||||||
|
|
||||||
|
- **ADCore.ps1**: CSPRNG password generation (Get-CryptoRandomInt, New-RandomPassword)
|
||||||
|
- **ADOrganizationalUnit.ps1**: OU ensure/compare
|
||||||
|
- **ADGroup.ps1**: Security group ensure/compare with accidental-deletion protection
|
||||||
|
- **ADUser.ps1**: User account ensure/compare with optional property management
|
||||||
|
- **ADDelegation.ps1**: OU delegation ACLs (schema GUIDs, ACE generation, bitwise subset checking)
|
||||||
|
- **ADPasswordPolicy.ps1**: Fine-grained password policy (PSO) ensure/compare
|
||||||
|
|
||||||
|
### Group Protection
|
||||||
|
|
||||||
|
Security groups now get `ProtectedFromAccidentalDeletion = $true` on creation. Existing unprotected groups are remediated on apply. Drift detection in TestOnly mode.
|
||||||
|
|
||||||
|
### Extended User Properties
|
||||||
|
|
||||||
|
User definitions support optional AD attributes (Description, Title, Department, Mail, etc.) via a `-Properties` hashtable. Core schema keys are explicit parameters; everything else flows through Properties. Works for both new user creation and existing user updates.
|
||||||
|
|
||||||
|
### Fine-Grained Password Policies (PSOs)
|
||||||
|
|
||||||
|
New `password-policies.ps1` definition file with two admin-tier policies:
|
||||||
|
|
||||||
|
- **PSO-MasterAdmins**: 16-char minimum, 30-day max age, 48 history, 3-attempt lockout (precedence 10)
|
||||||
|
- **PSO-DelegatedAdmins**: 12-char minimum, 42-day max age, 24 history, 5-attempt lockout (precedence 20)
|
||||||
|
|
||||||
|
Both override the Default Domain Policy for their linked groups. Property-level drift detection and full AppliesTo group linkage sync.
|
||||||
|
|
||||||
|
### Stale Object Detection
|
||||||
|
|
||||||
|
New `Get-StaleADObjects.ps1` read-only reporting script. Scans managed OUs for:
|
||||||
|
|
||||||
|
- Stale user accounts (no login in N days, default 90)
|
||||||
|
- Unexpectedly disabled accounts (not intentionally disabled in definitions)
|
||||||
|
- Empty security groups (zero members)
|
||||||
|
- Unmanaged users/groups (in managed OUs but not in definition files)
|
||||||
|
- Pending credential files with age
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Updated FRAMEWORK.md with modular AD architecture, PSO definition format, extended user properties
|
||||||
|
- Updated CLAUDE.md and README.md with new repo structure and features
|
||||||
|
- Updated function pairs table and dependency ordering
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [2.0.0] - 2026-02-14
|
||||||
|
|
||||||
|
Major expansion of the GPO framework. Modular library architecture, 8 new subsystems, and hardening policies deployed across all GPOs.
|
||||||
|
|
||||||
|
### GPO Framework -- Modular Architecture
|
||||||
|
|
||||||
|
Refactored `GPOHelper.ps1` from a monolithic library into a loader that dot-sources 12 specialized modules:
|
||||||
|
|
||||||
|
- **GPOCore.ps1**: SYSVOL paths, version bump, extension GUIDs, DSC helpers
|
||||||
|
- **GPOPolicy.ps1**: Security policy (GptTmpl.inf), registry settings, restricted groups
|
||||||
|
- **GPOPermissions.ps1**: GPO links (with order/enforcement), management permissions, security filtering
|
||||||
|
- **GPOScripts.ps1**: Startup/shutdown/logon/logoff script deployment to SYSVOL
|
||||||
|
- **GPOAudit.ps1**: Advanced audit policy (53 subcategories in audit.csv)
|
||||||
|
- **GPOPreferences.ps1**: Group Policy Preferences XML (10 types -- see below)
|
||||||
|
- **GPOWmiFilter.ps1**: WMI filter creation and GPO linking
|
||||||
|
- **GPOBackup.ps1**: Pre-apply backup with timestamped snapshots, restore via `Restore-GPOBaseline.ps1`
|
||||||
|
- **GPOFirewall.ps1**: Windows Firewall rules (`Open-NetGPO` session) and profile management
|
||||||
|
- **GPOAppLocker.ps1**: AppLocker policy management via `Set-AppLockerPolicy -LDAP`
|
||||||
|
- **GPOWdac.ps1**: WDAC policy deployment (.xml auto-converted to .p7b via `ConvertFrom-CIPolicy`)
|
||||||
|
- **GPOFolderRedirection.ps1**: Folder redirection via fdeploy1.ini (12 supported folders)
|
||||||
|
|
||||||
|
### GPO Preferences (10 types)
|
||||||
|
|
||||||
|
- ScheduledTasks, DriveMaps, EnvironmentVariables, Services (from 1.0)
|
||||||
|
- **Printers**: Shared printer mapping with default/skip-local options
|
||||||
|
- **Shortcuts**: Desktop/Start Menu shortcuts (URL, filesystem, shell)
|
||||||
|
- **Files**: File copy/replace from UNC or local paths
|
||||||
|
- **NetworkShares**: Local share creation with permissions
|
||||||
|
- **RegistryItems**: GPP registry items with action modes (distinct from Administrative Templates)
|
||||||
|
- **LocalUsersAndGroups**: Additive local group membership management (ADD/REMOVE without full replace)
|
||||||
|
- All types support Item-Level Targeting (ILT) filters
|
||||||
|
|
||||||
|
### GPO Operations
|
||||||
|
|
||||||
|
- **Restore-GPOBaseline.ps1**: List and restore GPO backups by name and timestamp
|
||||||
|
- **Get-UnmanagedGPOs.ps1**: Discover orphan GPOs in AD not managed by the framework
|
||||||
|
- **Automatic backups**: Every apply creates timestamped snapshots (SYSVOL + AD attributes), 5 retained per GPO
|
||||||
|
- **GPO status management**: `DisableUserConfiguration` / `DisableComputerConfiguration` keys
|
||||||
|
|
||||||
|
### New GPO -- Servers-01
|
||||||
|
|
||||||
|
- Linked to ExampleServers OU with WMI filter (ProductType = 3)
|
||||||
|
- Full audit (30 advanced audit subcategories), PowerShell transcription + module logging
|
||||||
|
- Command-line in process creation events, 256 MB security log
|
||||||
|
- Firewall: default-deny inbound, allow WinRM/RDP/ICMP/SMB
|
||||||
|
- GPP LocalUsersAndGroups: MasterAdmins added to Remote Desktop Users
|
||||||
|
|
||||||
|
### Hardening Deployed to Existing GPOs
|
||||||
|
|
||||||
|
- **Firewall profiles + rules**: Servers-01, AdminWorkstations-01, Workstations-01 (default-deny inbound, allow management traffic)
|
||||||
|
- **Advanced audit policy**: Servers-01 (30 subcategories), AdminWorkstations-01 (27 subcategories including DPAPI)
|
||||||
|
- **AppLocker audit mode**: Workstations-01 and AdminWorkstations-01 (Exe/Msi/Script collections, Microsoft-signed + Program Files + Windows + admin unrestricted)
|
||||||
|
- **WDAC audit mode**: AdminWorkstations-01 (AllowMicrosoft baseline -- all Microsoft root CAs, WHQL drivers, multiple policy format for future supplemental policies)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- **FRAMEWORK.md**: Complete developer reference -- architecture, ensure/compare pattern, all 15 setting types with format documentation, encoding guide, how-to recipes
|
||||||
|
- Updated README.md with GPO capabilities table, full repo structure, Servers-01
|
||||||
|
- Updated CLAUDE.md with 12-module library structure
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- AppLocker XML element names must match rule type (FilePathRule, FileHashRule, not always FilePublisherRule)
|
||||||
|
- `Get-NetFirewallRule` uses `-PolicyStore` not `-GPOSession` for reading GPO firewall rules
|
||||||
|
- `Get-AppLockerPolicy -Domain` is a SwitchParameter (flag), not a string parameter
|
||||||
|
- XML comments cannot contain `--` (double hyphen) -- .NET XmlSerializer strictly enforces this
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-02-13
|
||||||
|
|
||||||
|
First stable release. Full infrastructure-as-code coverage for the example.internal domain.
|
||||||
|
|
||||||
|
### AD Object Management
|
||||||
|
|
||||||
|
- **Apply-ADBaseline.ps1**: Idempotent orchestration for OUs, security groups, and user accounts
|
||||||
|
- **ADHelper.ps1**: Shared functions -- CSPRNG password generation, OU/group/user ensure and compare
|
||||||
|
- **Credential handoff**: New user passwords saved to ACL-locked files, never printed to console
|
||||||
|
- **Stale credential warnings**: Files older than 24 hours trigger a warning banner
|
||||||
|
- **Dependency ordering**: OUs -> groups -> users -> membership sync
|
||||||
|
|
||||||
|
### Organizational Units
|
||||||
|
|
||||||
|
- ExampleUsers, ExampleWorkstations, ExampleServers, ExampleAdmins, ExampleAdminWorkstations
|
||||||
|
|
||||||
|
### Security Groups and Delegation
|
||||||
|
|
||||||
|
- **MasterAdmins**: Full Control on all managed OUs, GPO edit rights (self-healing)
|
||||||
|
- **DelegatedAdmins**: Scoped helpdesk in ExampleUsers (password reset, user properties)
|
||||||
|
- ACL delegation automated via `delegations.ps1` (Ensure/Compare pattern with AD schema GUIDs)
|
||||||
|
|
||||||
|
### Group Policy
|
||||||
|
|
||||||
|
- **Apply-GPOBaseline.ps1**: Declarative GPO management -- security policy, registry settings, links, security filtering, management permissions
|
||||||
|
- **GPOHelper.ps1**: SYSVOL read/write, GptTmpl.inf parsing, GPO versioning, permission management
|
||||||
|
- **-GpUpdate switch**: Optional `gpupdate /force` after applying
|
||||||
|
- **-TestOnly mode**: Drift detection across all GPO settings without changes
|
||||||
|
- **Self-healing permissions**: MasterAdmins edit rights enforced on every run
|
||||||
|
|
||||||
|
### GPO Policies
|
||||||
|
|
||||||
|
- **Default Domain Policy**: Password (7-char min, 42-day max, 24 history), lockout (5 attempts, 30-min), Kerberos (10-hour TGT)
|
||||||
|
- **Default Domain Controllers Policy**: 25 user rights assignments, SMB/LDAP signing, secure channel encryption
|
||||||
|
- **Admins-01**: 10-min session lock, PowerShell script block logging + transcription, taskbar cleanup
|
||||||
|
- **Users-01**: Desktop lockdown (regedit, cmd, Run disabled), DelegatedAdmins exempted via deny security filtering
|
||||||
|
- **Workstations-01**: Full audit, autorun disabled, Windows Update 3 AM daily, NLA required, log sizing
|
||||||
|
- **AdminWorkstations-01**: Enhanced PAW -- all audit categories, PS transcription + module logging, command-line in 4688 events, 256 MB security log, Defender exclusions for JetBrains, RSAT startup script
|
||||||
|
|
||||||
|
### DSC Compliance
|
||||||
|
|
||||||
|
- **Apply-DscBaseline.ps1**: Second-layer validation of DC local state against GPO definitions
|
||||||
|
- **Single source of truth**: DSC configs read from settings.ps1, no value duplication
|
||||||
|
- **Kerberos validation**: Custom Script resource using secedit export (SecurityPolicyDsc doesn't support Kerberos natively)
|
||||||
|
- **Detailed drift output**: Reports specific non-compliant resources
|
||||||
|
- **Apply mode safety**: Warning banner + confirmation prompt required
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- README.md with architecture, workflow, security model, and operations guide
|
||||||
|
- Per-GPO README files with settings tables and design rationale
|
||||||
|
- CLAUDE.md for AI assistant context
|
||||||
1398
FRAMEWORK.md
Normal file
1398
FRAMEWORK.md
Normal file
File diff suppressed because it is too large
Load Diff
254
README.md
Normal file
254
README.md
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
# Declarative AD Framework -- Infrastructure as Code
|
||||||
|
|
||||||
|
Declarative management of Active Directory objects and Group Policy for Windows Server domains. All configuration is defined in PowerShell data files and applied idempotently via orchestration scripts.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Admin Workstation Domain Controller
|
||||||
|
+--------------------------+ +------------------------------+
|
||||||
|
| Edit definitions | git | Pull, test, apply |
|
||||||
|
| Push to remote | ------> | GPO + AD baseline scripts |
|
||||||
|
+--------------------------+ +------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**Two management domains:**
|
||||||
|
|
||||||
|
| Subsystem | Scripts | What it manages |
|
||||||
|
|---|---|---|
|
||||||
|
| AD Objects | `ad-objects/Apply-ADBaseline.ps1` | OUs, security groups, user accounts, delegation ACLs, password policies (PSOs) |
|
||||||
|
| Group Policy | `gpo/Apply-GPOBaseline.ps1` | GPOs via 12 modular libraries (see below) |
|
||||||
|
| DC Compliance | `gpo/Apply-DscBaseline.ps1` | Validates DC local state matches GPO definitions |
|
||||||
|
| AD Hygiene | `ad-objects/Get-StaleADObjects.ps1` | Stale accounts, orphans, unmanaged objects report |
|
||||||
|
| GPO Operations | `Restore-GPOBaseline.ps1`, `Get-UnmanagedGPOs.ps1` | Backup/restore, orphan detection |
|
||||||
|
|
||||||
|
### GPO Capabilities
|
||||||
|
|
||||||
|
The framework manages 15 setting types through 12 modular libraries:
|
||||||
|
|
||||||
|
| Capability | Library | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| Security policy | GPOPolicy.ps1 | GptTmpl.inf: password, lockout, Kerberos, user rights, security options |
|
||||||
|
| Registry settings | GPOPolicy.ps1 | Administrative Template values via `Set-GPRegistryValue` |
|
||||||
|
| Restricted groups | GPOPolicy.ps1 | Local group membership enforcement |
|
||||||
|
| GPO links | GPOPermissions.ps1 | OU linking with order and enforcement |
|
||||||
|
| Security filtering | GPOPermissions.ps1 | Deny Apply ACEs for group exemptions |
|
||||||
|
| Scripts | GPOScripts.ps1 | Startup/shutdown/logon/logoff script deployment |
|
||||||
|
| Advanced audit | GPOAudit.ps1 | 53 subcategory-level audit settings |
|
||||||
|
| Preferences | GPOPreferences.ps1 | 10 GPP types (tasks, drives, printers, shortcuts, etc.) |
|
||||||
|
| WMI filters | GPOWmiFilter.ps1 | OS-targeting filters |
|
||||||
|
| Backup/restore | GPOBackup.ps1 | Pre-apply snapshots with rollback |
|
||||||
|
| Firewall | GPOFirewall.ps1 | Rules and profile defaults |
|
||||||
|
| AppLocker | GPOAppLocker.ps1 | Application whitelisting (audit or enforce) |
|
||||||
|
| WDAC | GPOWdac.ps1 | Kernel-level code integrity policy |
|
||||||
|
| Folder redirection | GPOFolderRedirection.ps1 | User folder redirection to network paths |
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Windows Server 2016+ domain controller
|
||||||
|
- RSAT (Remote Server Administration Tools) on admin workstation
|
||||||
|
- Windows PowerShell 5.1
|
||||||
|
- PowerShell modules: `GroupPolicy`, `ActiveDirectory`, `SecurityPolicyDsc` (for DSC validation)
|
||||||
|
|
||||||
|
### Customization
|
||||||
|
|
||||||
|
1. **Clone the repo** and update the definition files for your domain:
|
||||||
|
|
||||||
|
- `ad-objects/ous.ps1` -- set `$domainDN` to your domain's distinguished name, rename OUs
|
||||||
|
- `ad-objects/groups.ps1` -- define your security groups and members
|
||||||
|
- `ad-objects/users.ps1` -- define your user accounts
|
||||||
|
- `ad-objects/delegations.ps1` -- define your OU delegation ACLs
|
||||||
|
- `ad-objects/password-policies.ps1` -- define fine-grained password policies
|
||||||
|
- `gpo/*/settings.ps1` -- update `LinkTo` paths with your OU DNs, update NETBIOS group references in `RestrictedGroups`
|
||||||
|
|
||||||
|
2. **Test first**, then apply:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\ad-objects\Apply-ADBaseline.ps1 -TestOnly # Drift detection, no changes
|
||||||
|
.\ad-objects\Apply-ADBaseline.ps1 # Apply AD objects
|
||||||
|
|
||||||
|
.\gpo\Apply-GPOBaseline.ps1 -TestOnly # Drift detection, no changes
|
||||||
|
.\gpo\Apply-GPOBaseline.ps1 -GpUpdate # Apply GPO settings + gpupdate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding a new GPO
|
||||||
|
|
||||||
|
Create a directory under `gpo/` with a `settings.ps1` -- auto-discovered on next run. No code changes needed.
|
||||||
|
|
||||||
|
## Example Security Model
|
||||||
|
|
||||||
|
The included example definitions demonstrate a tiered admin model:
|
||||||
|
|
||||||
|
| Tier | Account | Group | Access |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Break-glass | `Administrator` | Domain Admins | Full domain control -- emergency only |
|
||||||
|
| Tier 0 (operations) | `t0admin` | MasterAdmins | Full control on managed OUs, GPO edit, DNS, RDP to DC |
|
||||||
|
| Tier 2 (helpdesk) | `jsmith` | DelegatedAdmins | Password resets, user properties in ExampleUsers only |
|
||||||
|
|
||||||
|
- **Domain Admins** is reserved for break-glass scenarios only
|
||||||
|
- **MasterAdmins** has self-healing edit rights on all managed GPOs (no DA required)
|
||||||
|
- **DelegatedAdmins** are exempted from user desktop lockdown via GPO security filtering
|
||||||
|
- Fine-grained password policies (PSOs) enforce stricter requirements for admin tiers
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Day-to-day operations
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Edit definitions on your admin workstation
|
||||||
|
- AD objects: ad-objects/ous.ps1, groups.ps1, users.ps1
|
||||||
|
- GPO settings: gpo/<gpo-name>/settings.ps1
|
||||||
|
|
||||||
|
2. Commit and push
|
||||||
|
git add -A && git commit -m "description" && git push origin master
|
||||||
|
|
||||||
|
3. RDP to DC
|
||||||
|
mstsc /v:dc01.example.internal
|
||||||
|
|
||||||
|
4. Pull and test
|
||||||
|
cd C:\declarative-ad-framework
|
||||||
|
git pull origin master
|
||||||
|
.\ad-objects\Apply-ADBaseline.ps1 -TestOnly
|
||||||
|
.\gpo\Apply-GPOBaseline.ps1 -TestOnly
|
||||||
|
|
||||||
|
5. Review drift output, then apply
|
||||||
|
.\ad-objects\Apply-ADBaseline.ps1
|
||||||
|
.\gpo\Apply-GPOBaseline.ps1 -GpUpdate
|
||||||
|
|
||||||
|
6. Confirm DC compliance
|
||||||
|
.\gpo\Apply-DscBaseline.ps1 -TestOnly
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding a new user
|
||||||
|
|
||||||
|
1. Edit `ad-objects/users.ps1` -- add a hashtable with SamAccountName, Name, Path, MemberOf
|
||||||
|
2. Push to git, pull on DC
|
||||||
|
3. Run `.\ad-objects\Apply-ADBaseline.ps1` -- creates the user with a CSPRNG password
|
||||||
|
4. Read the password from `ad-objects/.credentials/<username>.txt`
|
||||||
|
5. Securely share the password with the user, then delete the file
|
||||||
|
6. User must change password on first login
|
||||||
|
|
||||||
|
### Adding a new GPO setting
|
||||||
|
|
||||||
|
1. Edit `gpo/<gpo-name>/settings.ps1` -- add to SecurityPolicy, RegistrySettings, FirewallRules, AppLockerPolicy, etc.
|
||||||
|
2. Push to git, pull on DC
|
||||||
|
3. Run `.\gpo\Apply-GPOBaseline.ps1 -GpUpdate`
|
||||||
|
|
||||||
|
### Creating a new GPO
|
||||||
|
|
||||||
|
1. Create `gpo/<new-name>/settings.ps1` with GPOName, LinkTo, SecurityPolicy, RegistrySettings
|
||||||
|
2. Push to git, pull on DC
|
||||||
|
3. Run `.\gpo\Apply-GPOBaseline.ps1 -GpUpdate` -- auto-creates the GPO, applies settings, links to OU
|
||||||
|
|
||||||
|
## Key Flags
|
||||||
|
|
||||||
|
| Flag | Script | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| `-TestOnly` | Both | Drift detection, no changes (always run first) |
|
||||||
|
| `-GpUpdate` | GPO | Runs `gpupdate /force` after applying |
|
||||||
|
| `-NoBackup` | GPO | Skip automatic pre-apply backup |
|
||||||
|
| `-NoCleanup` | GPO | Keep stale registry values instead of removing them |
|
||||||
|
|
||||||
|
## Example GPO Inventory
|
||||||
|
|
||||||
|
| GPO | Linked To | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| Default Domain Policy | Domain root | Password, lockout, Kerberos policies |
|
||||||
|
| Default Domain Controllers Policy | Domain Controllers OU | User rights assignments, security options |
|
||||||
|
| Admins-01 | ExampleAdmins OU | Session lock, PS logging, taskbar cleanup |
|
||||||
|
| Users-01 | ExampleUsers OU | Desktop lockdown (DelegatedAdmins exempted) |
|
||||||
|
| Workstations-01 | ExampleWorkstations OU | Audit, autorun, Windows Update, NLA, firewall, AppLocker (audit) |
|
||||||
|
| AdminWorkstations-01 | ExampleAdminWorkstations OU | PAW: full audit, PS transcription, firewall, AppLocker (audit), WDAC (audit) |
|
||||||
|
| Servers-01 | ExampleServers OU | Server hardening: full audit, PS transcription, firewall, GPP local groups |
|
||||||
|
|
||||||
|
## DSC Validation
|
||||||
|
|
||||||
|
`Apply-DscBaseline.ps1` is a second-layer check that validates the DC's **actual local state** against what the GPOs should have applied. It catches issues that the GPO baseline can't see -- processing failures, conflicting policies, or settings that were applied out-of-band.
|
||||||
|
|
||||||
|
```
|
||||||
|
Layer 1 (GPO Baseline): "Is the policy definition in SYSVOL correct?"
|
||||||
|
Layer 2 (DSC Baseline): "Did the DC actually apply it?"
|
||||||
|
```
|
||||||
|
|
||||||
|
Both DSC configurations read from their respective `settings.ps1` files -- single source of truth, no value duplication.
|
||||||
|
|
||||||
|
**Important:** DSC apply mode writes directly to the local security database, bypassing GPO. It requires typing `APPLY` to confirm and should only be used for emergency remediation.
|
||||||
|
|
||||||
|
## Bootstrap
|
||||||
|
|
||||||
|
The first run of `Apply-GPOBaseline.ps1` must be executed as **Administrator** (Domain Admins) to grant MasterAdmins edit rights on the managed GPOs. After that, MasterAdmins is self-maintaining.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
runas /user:EXAMPLE\Administrator "powershell.exe -ExecutionPolicy Bypass -Command cd C:\declarative-ad-framework\gpo; .\Apply-GPOBaseline.ps1 -GpUpdate"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recovery
|
||||||
|
|
||||||
|
If the default GPOs become corrupted beyond repair:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dcgpofix /target:domain # Reset Default Domain Policy
|
||||||
|
dcgpofix /target:dc # Reset Default Domain Controllers Policy
|
||||||
|
```
|
||||||
|
|
||||||
|
Then re-run `Apply-GPOBaseline.ps1` to reapply all settings.
|
||||||
|
|
||||||
|
## Repo Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
declarative-ad-framework/
|
||||||
|
README.md # This file
|
||||||
|
CHANGELOG.md # Version history
|
||||||
|
FRAMEWORK.md # Developer reference for extending the framework
|
||||||
|
ad-objects/
|
||||||
|
Apply-ADBaseline.ps1 # Orchestration: OUs -> groups -> users -> membership -> delegations -> PSOs
|
||||||
|
Get-StaleADObjects.ps1 # Read-only: stale accounts, orphans, unmanaged objects
|
||||||
|
ous.ps1 # OU definitions
|
||||||
|
groups.ps1 # Security group definitions
|
||||||
|
users.ps1 # User account definitions (supports optional properties)
|
||||||
|
delegations.ps1 # OU delegation rules (ACLs)
|
||||||
|
password-policies.ps1 # Fine-grained password policy (PSO) definitions
|
||||||
|
lib/
|
||||||
|
ADHelper.ps1 # Loader: dot-sources the 6 modules below
|
||||||
|
ADCore.ps1 # CSPRNG password generation
|
||||||
|
ADOrganizationalUnit.ps1 # OU ensure/compare
|
||||||
|
ADGroup.ps1 # Security group ensure/compare
|
||||||
|
ADUser.ps1 # User account ensure/compare
|
||||||
|
ADDelegation.ps1 # OU delegation ACLs
|
||||||
|
ADPasswordPolicy.ps1 # Fine-grained password policy ensure/compare
|
||||||
|
.credentials/ # Temp password files (gitignored, ACL-locked)
|
||||||
|
gpo/
|
||||||
|
Apply-GPOBaseline.ps1 # Orchestration: GPO settings to AD
|
||||||
|
Apply-DscBaseline.ps1 # DC-local compliance (DSC, test-only)
|
||||||
|
Restore-GPOBaseline.ps1 # List/restore GPO backups
|
||||||
|
Get-UnmanagedGPOs.ps1 # Discover orphan GPOs not managed by framework
|
||||||
|
lib/
|
||||||
|
GPOHelper.ps1 # Loader: dot-sources the 12 modules below
|
||||||
|
GPOCore.ps1 # SYSVOL paths, version bump, extension GUIDs, DSC helpers
|
||||||
|
GPOPolicy.ps1 # Security policy (GptTmpl.inf) + registry + restricted groups
|
||||||
|
GPOPermissions.ps1 # GPO links, management permissions, security filtering
|
||||||
|
GPOScripts.ps1 # Startup/shutdown/logon/logoff script deployment
|
||||||
|
GPOAudit.ps1 # Advanced audit policy (audit.csv)
|
||||||
|
GPOPreferences.ps1 # Group Policy Preferences XML (10 types)
|
||||||
|
GPOWmiFilter.ps1 # WMI filter creation + GPO linking
|
||||||
|
GPOBackup.ps1 # Pre-apply backup + restore functions
|
||||||
|
GPOFirewall.ps1 # Windows Firewall rules + profile management
|
||||||
|
GPOAppLocker.ps1 # AppLocker policy management
|
||||||
|
GPOWdac.ps1 # WDAC policy deployment
|
||||||
|
GPOFolderRedirection.ps1 # Folder redirection (fdeploy1.ini)
|
||||||
|
default-domain/ # Default Domain Policy + DSC config
|
||||||
|
default-domain-controller/ # Default DC Policy + DSC config
|
||||||
|
admins-01/ # Admin session/logging policy
|
||||||
|
users-01/ # User desktop lockdown
|
||||||
|
workstations-01/ # Workstation hardening + AppLocker audit
|
||||||
|
adminworkstations-01/ # PAW: forensics, AppLocker audit, WDAC audit
|
||||||
|
servers-01/ # Server hardening + GPP local groups
|
||||||
|
backups/ # Pre-apply GPO snapshots (gitignored)
|
||||||
|
Output/ # Compiled MOF files (gitignored)
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is provided as-is for educational and operational use. Adapt the definition files to your environment.
|
||||||
231
ad-objects/Apply-ADBaseline.ps1
Normal file
231
ad-objects/Apply-ADBaseline.ps1
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
# Apply-ADBaseline.ps1
|
||||||
|
# Orchestration script for managing AD objects (OUs, groups, users).
|
||||||
|
#
|
||||||
|
# Reads declarative definitions from ous.ps1, groups.ps1, users.ps1,
|
||||||
|
# delegations.ps1, and password-policies.ps1, then compares against
|
||||||
|
# live AD and applies changes. Works from any machine with RSAT.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# .\Apply-ADBaseline.ps1 # Apply all AD objects
|
||||||
|
# .\Apply-ADBaseline.ps1 -TestOnly # Compare desired vs. current (no changes)
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[switch]$TestOnly
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$ScriptRoot = $PSScriptRoot
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Load helper functions
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
. (Join-Path $ScriptRoot 'lib\ADHelper.ps1')
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Load definitions
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
$ous = & (Join-Path $ScriptRoot 'ous.ps1')
|
||||||
|
$groups = & (Join-Path $ScriptRoot 'groups.ps1')
|
||||||
|
$users = & (Join-Path $ScriptRoot 'users.ps1')
|
||||||
|
$delegations = & (Join-Path $ScriptRoot 'delegations.ps1')
|
||||||
|
$passwordPolicies = & (Join-Path $ScriptRoot 'password-policies.ps1')
|
||||||
|
|
||||||
|
$totalDiffs = 0
|
||||||
|
$createdUsers = @()
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Pre-flight: Warn about stale credential files (older than 24 hours)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
$credDir = Join-Path $ScriptRoot '.credentials'
|
||||||
|
if (Test-Path $credDir) {
|
||||||
|
$staleFiles = Get-ChildItem -Path $credDir -Filter '*.txt' |
|
||||||
|
Where-Object { $_.LastWriteTime -lt (Get-Date).AddHours(-24) }
|
||||||
|
if ($staleFiles.Count -gt 0) {
|
||||||
|
Write-Host '========================================================' -ForegroundColor Red
|
||||||
|
Write-Host ' WARNING: Stale credential files detected (>24 hours)' -ForegroundColor Red
|
||||||
|
Write-Host '========================================================' -ForegroundColor Red
|
||||||
|
foreach ($f in $staleFiles) {
|
||||||
|
$age = [math]::Round(((Get-Date) - $f.LastWriteTime).TotalHours, 1)
|
||||||
|
Write-Host " - $($f.Name) (${age}h old)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
Write-Host ' These should have been handed off and deleted.' -ForegroundColor Red
|
||||||
|
Write-Host '========================================================' -ForegroundColor Red
|
||||||
|
Write-Host ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Step 1: Organizational Units (must exist before groups/users)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
Write-Host "=== Organizational Units ===" -ForegroundColor White
|
||||||
|
|
||||||
|
foreach ($ou in $ous) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
$diff = Compare-ADOU -Name $ou.Name -Path $ou.Path
|
||||||
|
if ($diff) { $totalDiffs++ }
|
||||||
|
} else {
|
||||||
|
Ensure-ADOU -Name $ou.Name -Path $ou.Path -Description $ou.Description | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Step 2: Security Groups (must exist before users can join them)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
Write-Host "=== Security Groups ===" -ForegroundColor White
|
||||||
|
|
||||||
|
foreach ($group in $groups) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
$diffs = Compare-ADSecurityGroup -Name $group.Name -Members $group.Members
|
||||||
|
$totalDiffs += @($diffs).Count
|
||||||
|
} else {
|
||||||
|
$params = @{
|
||||||
|
Name = $group.Name
|
||||||
|
Path = $group.Path
|
||||||
|
Description = $group.Description
|
||||||
|
Scope = $group.Scope
|
||||||
|
}
|
||||||
|
# Don't sync members yet - users may not exist
|
||||||
|
Ensure-ADSecurityGroup @params | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Step 3: User Accounts
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
Write-Host "=== User Accounts ===" -ForegroundColor White
|
||||||
|
|
||||||
|
foreach ($user in $users) {
|
||||||
|
# Extract optional properties (anything not in the core schema)
|
||||||
|
$coreKeys = @('SamAccountName','Name','GivenName','Surname','Path','Enabled','MemberOf')
|
||||||
|
$optionalProps = @{}
|
||||||
|
foreach ($key in $user.Keys) {
|
||||||
|
if ($key -notin $coreKeys) {
|
||||||
|
$optionalProps[$key] = $user[$key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($TestOnly) {
|
||||||
|
$diffs = Compare-ADUserAccount `
|
||||||
|
-SamAccountName $user.SamAccountName `
|
||||||
|
-Path $user.Path `
|
||||||
|
-Enabled $user.Enabled `
|
||||||
|
-MemberOf $user.MemberOf `
|
||||||
|
-Properties $optionalProps
|
||||||
|
$totalDiffs += @($diffs).Count
|
||||||
|
} else {
|
||||||
|
$wasCreated = Ensure-ADUserAccount `
|
||||||
|
-SamAccountName $user.SamAccountName `
|
||||||
|
-Name $user.Name `
|
||||||
|
-GivenName $user.GivenName `
|
||||||
|
-Surname $user.Surname `
|
||||||
|
-Path $user.Path `
|
||||||
|
-Enabled $user.Enabled `
|
||||||
|
-MemberOf $user.MemberOf `
|
||||||
|
-Properties $optionalProps
|
||||||
|
if ($wasCreated) {
|
||||||
|
$createdUsers += $user.SamAccountName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Step 4: Sync group membership (now that users exist)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
if (-not $TestOnly) {
|
||||||
|
Write-Host "=== Group Membership Sync ===" -ForegroundColor White
|
||||||
|
|
||||||
|
foreach ($group in $groups) {
|
||||||
|
if ($group.Members.Count -gt 0) {
|
||||||
|
Ensure-ADSecurityGroup -Name $group.Name -Path $group.Path -Scope $group.Scope -Members $group.Members | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Step 5: OU Delegation (ACLs)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
Write-Host "=== OU Delegation ===" -ForegroundColor White
|
||||||
|
|
||||||
|
foreach ($delegation in $delegations) {
|
||||||
|
foreach ($ou in $delegation.TargetOUs) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
$diffs = Compare-OUDelegation `
|
||||||
|
-GroupName $delegation.GroupName `
|
||||||
|
-TargetOU $ou `
|
||||||
|
-Rights $delegation.Rights
|
||||||
|
$totalDiffs += @($diffs).Count
|
||||||
|
} else {
|
||||||
|
Ensure-OUDelegation `
|
||||||
|
-GroupName $delegation.GroupName `
|
||||||
|
-TargetOU $ou `
|
||||||
|
-Rights $delegation.Rights | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Step 6: Fine-Grained Password Policies (PSOs)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
Write-Host "=== Password Policies ===" -ForegroundColor White
|
||||||
|
|
||||||
|
foreach ($pso in $passwordPolicies) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
$diffs = Compare-ADPasswordPolicy -Definition $pso
|
||||||
|
$totalDiffs += @($diffs).Count
|
||||||
|
} else {
|
||||||
|
Ensure-ADPasswordPolicy -Definition $pso
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Summary
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
if ($TestOnly) {
|
||||||
|
if ($totalDiffs -eq 0) {
|
||||||
|
Write-Host 'All AD objects match desired state.' -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "$totalDiffs difference(s) found." -ForegroundColor Red
|
||||||
|
}
|
||||||
|
Write-Host 'Compliance test complete. No changes were made.' -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host 'All AD objects applied.' -ForegroundColor Green
|
||||||
|
|
||||||
|
# --- Credential handoff warning ---
|
||||||
|
if ($createdUsers.Count -gt 0) {
|
||||||
|
$credDir = Join-Path $ScriptRoot '.credentials'
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host '========================================================' -ForegroundColor Magenta
|
||||||
|
Write-Host ' ACTION REQUIRED: New user credentials pending handoff' -ForegroundColor Magenta
|
||||||
|
Write-Host '========================================================' -ForegroundColor Magenta
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host " $($createdUsers.Count) user(s) were created with temporary passwords." -ForegroundColor Yellow
|
||||||
|
Write-Host ' Passwords are stored in:' -ForegroundColor Yellow
|
||||||
|
Write-Host " $credDir" -ForegroundColor White
|
||||||
|
Write-Host ''
|
||||||
|
foreach ($u in $createdUsers) {
|
||||||
|
Write-Host " - $u.txt" -ForegroundColor White
|
||||||
|
}
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host ' You must:' -ForegroundColor Yellow
|
||||||
|
Write-Host ' 1. Read each credential file' -ForegroundColor White
|
||||||
|
Write-Host ' 2. Securely share the password with the end user' -ForegroundColor White
|
||||||
|
Write-Host ' 3. Delete the credential file after handoff' -ForegroundColor White
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host ' These files are ACL-locked to your account only.' -ForegroundColor DarkGray
|
||||||
|
Write-Host ' Users must change their password on first login.' -ForegroundColor DarkGray
|
||||||
|
Write-Host '========================================================' -ForegroundColor Magenta
|
||||||
|
}
|
||||||
|
}
|
||||||
202
ad-objects/Get-StaleADObjects.ps1
Normal file
202
ad-objects/Get-StaleADObjects.ps1
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
# Get-StaleADObjects.ps1
|
||||||
|
# Read-only reporting script -- discovers stale, orphaned, and unmanaged
|
||||||
|
# AD objects in managed OUs.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# .\Get-StaleADObjects.ps1 # Default 90-day threshold
|
||||||
|
# .\Get-StaleADObjects.ps1 -StaleDays 60 # Custom threshold
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[int]$StaleDays = 90
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$ScriptRoot = $PSScriptRoot
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Load definitions (source of truth)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
$definedUsers = & (Join-Path $ScriptRoot 'users.ps1')
|
||||||
|
$definedGroups = & (Join-Path $ScriptRoot 'groups.ps1')
|
||||||
|
$definedOUs = & (Join-Path $ScriptRoot 'ous.ps1')
|
||||||
|
|
||||||
|
$managedUserNames = @($definedUsers | ForEach-Object { $_.SamAccountName })
|
||||||
|
$managedGroupNames = @($definedGroups | ForEach-Object { $_.Name })
|
||||||
|
$managedOUDNs = @($definedOUs | ForEach-Object { "OU=$($_.Name),$($_.Path)" })
|
||||||
|
|
||||||
|
$staleDate = (Get-Date).AddDays(-$StaleDays)
|
||||||
|
$totalFindings = 0
|
||||||
|
|
||||||
|
Write-Host "=== Stale AD Object Report ===" -ForegroundColor Cyan
|
||||||
|
Write-Host " Threshold: $StaleDays days (before $(Get-Date $staleDate -Format 'yyyy-MM-dd'))" -ForegroundColor DarkGray
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 1. Stale user accounts (no login in N days)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
Write-Host "--- Stale User Accounts (no login in $StaleDays days) ---" -ForegroundColor White
|
||||||
|
|
||||||
|
$staleCount = 0
|
||||||
|
foreach ($ouDN in $managedOUDNs) {
|
||||||
|
$users = Get-ADUser -Filter * -SearchBase $ouDN -SearchScope OneLevel `
|
||||||
|
-Properties LastLogonDate, Enabled -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
foreach ($user in $users) {
|
||||||
|
$isStale = $false
|
||||||
|
if ($null -eq $user.LastLogonDate) {
|
||||||
|
# Never logged in -- stale if account is old enough
|
||||||
|
$created = (Get-ADUser -Identity $user.SamAccountName -Properties WhenCreated).WhenCreated
|
||||||
|
if ($created -lt $staleDate) {
|
||||||
|
$isStale = $true
|
||||||
|
}
|
||||||
|
} elseif ($user.LastLogonDate -lt $staleDate) {
|
||||||
|
$isStale = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isStale -and $user.Enabled) {
|
||||||
|
$lastLogin = if ($null -eq $user.LastLogonDate) { 'Never' } else { Get-Date $user.LastLogonDate -Format 'yyyy-MM-dd' }
|
||||||
|
Write-Host " [STALE] $($user.SamAccountName) -- last login: $lastLogin" -ForegroundColor Yellow
|
||||||
|
$staleCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($staleCount -eq 0) {
|
||||||
|
Write-Host " No stale accounts found." -ForegroundColor Green
|
||||||
|
}
|
||||||
|
$totalFindings += $staleCount
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 2. Disabled accounts (not intentionally disabled in users.ps1)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
Write-Host "--- Unexpectedly Disabled Accounts ---" -ForegroundColor White
|
||||||
|
|
||||||
|
$disabledCount = 0
|
||||||
|
foreach ($ouDN in $managedOUDNs) {
|
||||||
|
$users = Get-ADUser -Filter { Enabled -eq $false } -SearchBase $ouDN -SearchScope OneLevel `
|
||||||
|
-ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
foreach ($user in $users) {
|
||||||
|
# Check if this user is intentionally disabled in definitions
|
||||||
|
$defined = $definedUsers | Where-Object { $_.SamAccountName -eq $user.SamAccountName }
|
||||||
|
if ($defined -and $defined.Enabled -eq $false) {
|
||||||
|
continue # Intentionally disabled
|
||||||
|
}
|
||||||
|
Write-Host " [DISABLED] $($user.SamAccountName) -- not marked as disabled in definitions" -ForegroundColor Yellow
|
||||||
|
$disabledCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($disabledCount -eq 0) {
|
||||||
|
Write-Host " No unexpectedly disabled accounts found." -ForegroundColor Green
|
||||||
|
}
|
||||||
|
$totalFindings += $disabledCount
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 3. Empty groups (zero members in managed OUs)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
Write-Host "--- Empty Groups ---" -ForegroundColor White
|
||||||
|
|
||||||
|
$emptyCount = 0
|
||||||
|
foreach ($ouDN in $managedOUDNs) {
|
||||||
|
$groups = Get-ADGroup -Filter * -SearchBase $ouDN -SearchScope OneLevel `
|
||||||
|
-ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
foreach ($group in $groups) {
|
||||||
|
$members = @(Get-ADGroupMember -Identity $group.SamAccountName -ErrorAction SilentlyContinue)
|
||||||
|
if ($members.Count -eq 0) {
|
||||||
|
Write-Host " [EMPTY] $($group.Name) -- zero members" -ForegroundColor Yellow
|
||||||
|
$emptyCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($emptyCount -eq 0) {
|
||||||
|
Write-Host " No empty groups found." -ForegroundColor Green
|
||||||
|
}
|
||||||
|
$totalFindings += $emptyCount
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 4. Unmanaged users (in managed OUs but not in users.ps1)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
Write-Host "--- Unmanaged Users (in managed OUs but not in definitions) ---" -ForegroundColor White
|
||||||
|
|
||||||
|
$unmanagedUserCount = 0
|
||||||
|
foreach ($ouDN in $managedOUDNs) {
|
||||||
|
$users = Get-ADUser -Filter * -SearchBase $ouDN -SearchScope OneLevel `
|
||||||
|
-ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
foreach ($user in $users) {
|
||||||
|
if ($user.SamAccountName -notin $managedUserNames) {
|
||||||
|
Write-Host " [UNMANAGED] $($user.SamAccountName) in $ouDN" -ForegroundColor Yellow
|
||||||
|
$unmanagedUserCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($unmanagedUserCount -eq 0) {
|
||||||
|
Write-Host " All users in managed OUs are defined." -ForegroundColor Green
|
||||||
|
}
|
||||||
|
$totalFindings += $unmanagedUserCount
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 5. Unmanaged groups (in managed OUs but not in groups.ps1)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
Write-Host "--- Unmanaged Groups (in managed OUs but not in definitions) ---" -ForegroundColor White
|
||||||
|
|
||||||
|
$unmanagedGroupCount = 0
|
||||||
|
foreach ($ouDN in $managedOUDNs) {
|
||||||
|
$groups = Get-ADGroup -Filter * -SearchBase $ouDN -SearchScope OneLevel `
|
||||||
|
-ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
foreach ($group in $groups) {
|
||||||
|
if ($group.Name -notin $managedGroupNames) {
|
||||||
|
Write-Host " [UNMANAGED] $($group.Name) in $ouDN" -ForegroundColor Yellow
|
||||||
|
$unmanagedGroupCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($unmanagedGroupCount -eq 0) {
|
||||||
|
Write-Host " All groups in managed OUs are defined." -ForegroundColor Green
|
||||||
|
}
|
||||||
|
$totalFindings += $unmanagedGroupCount
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# 6. Pending credential files
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
Write-Host "--- Pending Credential Files ---" -ForegroundColor White
|
||||||
|
|
||||||
|
$credDir = Join-Path $ScriptRoot '.credentials'
|
||||||
|
$credCount = 0
|
||||||
|
if (Test-Path $credDir) {
|
||||||
|
$credFiles = Get-ChildItem -Path $credDir -Filter '*.txt' -ErrorAction SilentlyContinue
|
||||||
|
foreach ($f in $credFiles) {
|
||||||
|
$age = [math]::Round(((Get-Date) - $f.LastWriteTime).TotalHours, 1)
|
||||||
|
$color = if ($age -gt 24) { 'Red' } else { 'Yellow' }
|
||||||
|
Write-Host " [PENDING] $($f.Name) -- ${age}h old" -ForegroundColor $color
|
||||||
|
$credCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($credCount -eq 0) {
|
||||||
|
Write-Host " No pending credential files." -ForegroundColor Green
|
||||||
|
}
|
||||||
|
$totalFindings += $credCount
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Summary
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
if ($totalFindings -eq 0) {
|
||||||
|
Write-Host "No findings. All managed OUs are clean." -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "$totalFindings finding(s) across all checks." -ForegroundColor Red
|
||||||
|
}
|
||||||
|
Write-Host 'Report complete. No changes were made.' -ForegroundColor DarkGray
|
||||||
46
ad-objects/delegations.ps1
Normal file
46
ad-objects/delegations.ps1
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# OU Delegation Definitions
|
||||||
|
# Processed after groups and users exist (Step 5).
|
||||||
|
# Defines what security groups get what permissions on which OUs.
|
||||||
|
#
|
||||||
|
# Rights syntax:
|
||||||
|
# 'FullControl' - GenericAll on the OU and all descendants
|
||||||
|
# 'ListContents' - ListChildren on the OU itself
|
||||||
|
# 'ReadAllProperties' - ReadProperty (all) on the OU itself
|
||||||
|
# 'ResetPassword' - Extended right on descendant user objects
|
||||||
|
# 'ReadWriteProperty:<attributeName>' - Read+Write a specific attribute on descendant user objects
|
||||||
|
|
||||||
|
$domainDN = 'DC=example,DC=internal'
|
||||||
|
|
||||||
|
@(
|
||||||
|
@{
|
||||||
|
GroupName = 'MasterAdmins'
|
||||||
|
TargetOUs = @(
|
||||||
|
"OU=ExampleUsers,$domainDN"
|
||||||
|
"OU=ExampleWorkstations,$domainDN"
|
||||||
|
"OU=ExampleServers,$domainDN"
|
||||||
|
"OU=ExampleAdmins,$domainDN"
|
||||||
|
"OU=ExampleAdminWorkstations,$domainDN"
|
||||||
|
)
|
||||||
|
Rights = 'FullControl'
|
||||||
|
}
|
||||||
|
|
||||||
|
@{
|
||||||
|
GroupName = 'DelegatedAdmins'
|
||||||
|
TargetOUs = @(
|
||||||
|
"OU=ExampleUsers,$domainDN"
|
||||||
|
)
|
||||||
|
Rights = @(
|
||||||
|
'ListContents'
|
||||||
|
'ReadAllProperties'
|
||||||
|
'ResetPassword'
|
||||||
|
'ReadWriteProperty:userAccountControl'
|
||||||
|
'ReadWriteProperty:lockoutTime'
|
||||||
|
'ReadWriteProperty:displayName'
|
||||||
|
'ReadWriteProperty:givenName'
|
||||||
|
'ReadWriteProperty:sn'
|
||||||
|
'ReadWriteProperty:mail'
|
||||||
|
'ReadWriteProperty:telephoneNumber'
|
||||||
|
'ReadWriteProperty:description'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
20
ad-objects/groups.ps1
Normal file
20
ad-objects/groups.ps1
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Security Group Definitions
|
||||||
|
# Processed after OUs, before user membership sync.
|
||||||
|
|
||||||
|
@(
|
||||||
|
@{
|
||||||
|
Name = 'MasterAdmins'
|
||||||
|
Path = 'OU=ExampleAdmins,DC=example,DC=internal'
|
||||||
|
Scope = 'Global'
|
||||||
|
Description = 'Master administrators - full control over all managed OUs'
|
||||||
|
Members = @('t0admin')
|
||||||
|
}
|
||||||
|
|
||||||
|
@{
|
||||||
|
Name = 'DelegatedAdmins'
|
||||||
|
Path = 'OU=ExampleAdmins,DC=example,DC=internal'
|
||||||
|
Scope = 'Global'
|
||||||
|
Description = 'Delegated administrators - scoped helpdesk privileges in ExampleUsers'
|
||||||
|
Members = @('jsmith')
|
||||||
|
}
|
||||||
|
)
|
||||||
55
ad-objects/lib/ADCore.ps1
Normal file
55
ad-objects/lib/ADCore.ps1
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# ADCore.ps1
|
||||||
|
# Shared utility functions used by multiple AD modules.
|
||||||
|
# Must be loaded before other AD modules (ADUser.ps1 depends on New-RandomPassword).
|
||||||
|
|
||||||
|
function Get-CryptoRandomInt {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Returns a cryptographically secure random integer in [0, $Max).
|
||||||
|
#>
|
||||||
|
param([int]$Max)
|
||||||
|
|
||||||
|
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
|
||||||
|
$bytes = [byte[]]::new(4)
|
||||||
|
$rng.GetBytes($bytes)
|
||||||
|
$rng.Dispose()
|
||||||
|
return [Math]::Abs([BitConverter]::ToInt32($bytes, 0)) % $Max
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-RandomPassword {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Generates a cryptographically secure random password that meets AD complexity requirements.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[int]$Length = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
$upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'
|
||||||
|
$lower = 'abcdefghjkmnpqrstuvwxyz'
|
||||||
|
$digits = '23456789'
|
||||||
|
$special = '!@#$%&*?'
|
||||||
|
|
||||||
|
# Guarantee at least one of each type
|
||||||
|
$chars = [System.Collections.Generic.List[char]]::new()
|
||||||
|
$chars.Add($upper[(Get-CryptoRandomInt -Max $upper.Length)])
|
||||||
|
$chars.Add($lower[(Get-CryptoRandomInt -Max $lower.Length)])
|
||||||
|
$chars.Add($digits[(Get-CryptoRandomInt -Max $digits.Length)])
|
||||||
|
$chars.Add($special[(Get-CryptoRandomInt -Max $special.Length)])
|
||||||
|
|
||||||
|
# Fill the rest randomly from all character sets
|
||||||
|
$all = $upper + $lower + $digits + $special
|
||||||
|
for ($i = $chars.Count; $i -lt $Length; $i++) {
|
||||||
|
$chars.Add($all[(Get-CryptoRandomInt -Max $all.Length)])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fisher-Yates shuffle using CSPRNG
|
||||||
|
for ($i = $chars.Count - 1; $i -gt 0; $i--) {
|
||||||
|
$j = Get-CryptoRandomInt -Max ($i + 1)
|
||||||
|
$temp = $chars[$i]
|
||||||
|
$chars[$i] = $chars[$j]
|
||||||
|
$chars[$j] = $temp
|
||||||
|
}
|
||||||
|
|
||||||
|
return -join $chars
|
||||||
|
}
|
||||||
232
ad-objects/lib/ADDelegation.ps1
Normal file
232
ad-objects/lib/ADDelegation.ps1
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
# ADDelegation.ps1
|
||||||
|
# OU delegation (ACL) management.
|
||||||
|
# Uses AD schema GUIDs to construct ActiveDirectoryAccessRule objects.
|
||||||
|
# No dependencies on other AD modules.
|
||||||
|
|
||||||
|
# AD Schema GUIDs -- well-known, universal across all AD forests.
|
||||||
|
# Verified against Microsoft AD Schema documentation.
|
||||||
|
# https://learn.microsoft.com/en-us/windows/win32/adschema/
|
||||||
|
|
||||||
|
$Script:ADClassGuid = @{
|
||||||
|
user = [Guid]'bf967aba-0de6-11d0-a285-00aa003049e2'
|
||||||
|
}
|
||||||
|
|
||||||
|
$Script:ADAttributeGuid = @{
|
||||||
|
userAccountControl = [Guid]'bf967a68-0de6-11d0-a285-00aa003049e2'
|
||||||
|
lockoutTime = [Guid]'28630ebf-41d5-11d1-a9c1-0000f80367c1'
|
||||||
|
displayName = [Guid]'bf967953-0de6-11d0-a285-00aa003049e2'
|
||||||
|
givenName = [Guid]'f0f8ff8e-1191-11d0-a060-00aa006c33ed'
|
||||||
|
sn = [Guid]'bf967a41-0de6-11d0-a285-00aa003049e2'
|
||||||
|
mail = [Guid]'bf967961-0de6-11d0-a285-00aa003049e2'
|
||||||
|
telephoneNumber = [Guid]'bf967a49-0de6-11d0-a285-00aa003049e2'
|
||||||
|
description = [Guid]'bf967950-0de6-11d0-a285-00aa003049e2'
|
||||||
|
}
|
||||||
|
|
||||||
|
$Script:ADExtendedRight = @{
|
||||||
|
ResetPassword = [Guid]'00299570-246d-11d0-a768-00aa006e0529'
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-DelegationACEs {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Converts a rights specification into ActiveDirectoryAccessRule objects.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[System.Security.Principal.SecurityIdentifier]$SID,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Rights
|
||||||
|
)
|
||||||
|
|
||||||
|
$aces = @()
|
||||||
|
$rightsArray = @($Rights)
|
||||||
|
|
||||||
|
foreach ($right in $rightsArray) {
|
||||||
|
if ($right -eq 'FullControl') {
|
||||||
|
$aces += New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
|
||||||
|
$SID,
|
||||||
|
[System.DirectoryServices.ActiveDirectoryRights]::GenericAll,
|
||||||
|
[System.Security.AccessControl.AccessControlType]::Allow,
|
||||||
|
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
|
||||||
|
)
|
||||||
|
}
|
||||||
|
elseif ($right -eq 'ListContents') {
|
||||||
|
$aces += New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
|
||||||
|
$SID,
|
||||||
|
[System.DirectoryServices.ActiveDirectoryRights]::ListChildren,
|
||||||
|
[System.Security.AccessControl.AccessControlType]::Allow,
|
||||||
|
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::None
|
||||||
|
)
|
||||||
|
}
|
||||||
|
elseif ($right -eq 'ReadAllProperties') {
|
||||||
|
$aces += New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
|
||||||
|
$SID,
|
||||||
|
[System.DirectoryServices.ActiveDirectoryRights]::ReadProperty,
|
||||||
|
[System.Security.AccessControl.AccessControlType]::Allow,
|
||||||
|
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::None
|
||||||
|
)
|
||||||
|
}
|
||||||
|
elseif ($right -eq 'ResetPassword') {
|
||||||
|
$aces += New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
|
||||||
|
$SID,
|
||||||
|
[System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight,
|
||||||
|
[System.Security.AccessControl.AccessControlType]::Allow,
|
||||||
|
$Script:ADExtendedRight['ResetPassword'],
|
||||||
|
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::Descendents,
|
||||||
|
$Script:ADClassGuid['user']
|
||||||
|
)
|
||||||
|
}
|
||||||
|
elseif ($right -match '^ReadWriteProperty:(.+)$') {
|
||||||
|
$attrName = $Matches[1]
|
||||||
|
|
||||||
|
if (-not $Script:ADAttributeGuid.ContainsKey($attrName)) {
|
||||||
|
throw "Unknown AD attribute '$attrName' in delegation rule. Add it to `$Script:ADAttributeGuid in ADDelegation.ps1."
|
||||||
|
}
|
||||||
|
|
||||||
|
$aces += New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
|
||||||
|
$SID,
|
||||||
|
([System.DirectoryServices.ActiveDirectoryRights]::ReadProperty -bor
|
||||||
|
[System.DirectoryServices.ActiveDirectoryRights]::WriteProperty),
|
||||||
|
[System.Security.AccessControl.AccessControlType]::Allow,
|
||||||
|
$Script:ADAttributeGuid[$attrName],
|
||||||
|
[System.DirectoryServices.ActiveDirectorySecurityInheritance]::Descendents,
|
||||||
|
$Script:ADClassGuid['user']
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw "Unknown delegation right: '$right'. Valid: FullControl, ListContents, ReadAllProperties, ResetPassword, ReadWriteProperty:<attribute>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $aces
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-ACEExists {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Returns $true if an equivalent ACE already exists in the given ACL.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[System.DirectoryServices.ActiveDirectorySecurity]$ACL,
|
||||||
|
[System.DirectoryServices.ActiveDirectoryAccessRule]$DesiredACE
|
||||||
|
)
|
||||||
|
|
||||||
|
$desiredSID = $DesiredACE.IdentityReference.Value
|
||||||
|
|
||||||
|
foreach ($existing in $ACL.Access) {
|
||||||
|
$existingSID = $null
|
||||||
|
try {
|
||||||
|
$existingSID = $existing.IdentityReference.Translate(
|
||||||
|
[System.Security.Principal.SecurityIdentifier]).Value
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($existingSID -ne $desiredSID) { continue }
|
||||||
|
# Bitwise subset check: AD merges compatible ACEs, so the stored
|
||||||
|
# rights may be a superset of what we asked for (e.g. ListChildren
|
||||||
|
# + ReadProperty merged into one ACE).
|
||||||
|
if (($existing.ActiveDirectoryRights -band $DesiredACE.ActiveDirectoryRights) -ne $DesiredACE.ActiveDirectoryRights) { continue }
|
||||||
|
if ($existing.AccessControlType -ne $DesiredACE.AccessControlType) { continue }
|
||||||
|
if ($existing.ObjectType -ne $DesiredACE.ObjectType) { continue }
|
||||||
|
if ($existing.InheritanceType -ne $DesiredACE.InheritanceType) { continue }
|
||||||
|
if ($existing.InheritedObjectType -ne $DesiredACE.InheritedObjectType) { continue }
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-OUDelegation {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Idempotently applies delegation ACEs to an OU for a given security group.
|
||||||
|
Returns the number of ACEs added (0 if already in desired state).
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GroupName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$TargetOU,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Rights
|
||||||
|
)
|
||||||
|
|
||||||
|
$group = Get-ADGroup -Identity $GroupName -ErrorAction Stop
|
||||||
|
$sid = New-Object System.Security.Principal.SecurityIdentifier($group.SID)
|
||||||
|
|
||||||
|
$desiredACEs = ConvertTo-DelegationACEs -SID $sid -Rights $Rights
|
||||||
|
|
||||||
|
$adPath = "AD:$TargetOU"
|
||||||
|
$acl = Get-Acl $adPath
|
||||||
|
|
||||||
|
$added = 0
|
||||||
|
foreach ($ace in $desiredACEs) {
|
||||||
|
if (-not (Test-ACEExists -ACL $acl -DesiredACE $ace)) {
|
||||||
|
$acl.AddAccessRule($ace)
|
||||||
|
$added++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($added -gt 0) {
|
||||||
|
Set-Acl $adPath $acl
|
||||||
|
Write-Host " [DELEGATED] $GroupName on $TargetOU ($added ACE(s) added)" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [OK] $GroupName delegation on $TargetOU" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
return $added
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-OUDelegation {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Checks if delegation ACEs exist for a group on an OU.
|
||||||
|
Returns diff objects for any missing ACEs.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GroupName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$TargetOU,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Rights
|
||||||
|
)
|
||||||
|
|
||||||
|
$diffs = @()
|
||||||
|
|
||||||
|
$group = Get-ADGroup -Identity $GroupName -ErrorAction Stop
|
||||||
|
$sid = New-Object System.Security.Principal.SecurityIdentifier($group.SID)
|
||||||
|
|
||||||
|
$desiredACEs = ConvertTo-DelegationACEs -SID $sid -Rights $Rights
|
||||||
|
|
||||||
|
$adPath = "AD:$TargetOU"
|
||||||
|
$acl = Get-Acl $adPath
|
||||||
|
|
||||||
|
$missing = 0
|
||||||
|
foreach ($ace in $desiredACEs) {
|
||||||
|
if (-not (Test-ACEExists -ACL $acl -DesiredACE $ace)) {
|
||||||
|
$missing++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($missing -eq 0) {
|
||||||
|
Write-Host " [OK] $GroupName delegation on $TargetOU" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [DRIFT] $GroupName missing $missing ACE(s) on $TargetOU" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'Delegation'
|
||||||
|
Group = $GroupName
|
||||||
|
TargetOU = $TargetOU
|
||||||
|
Status = "Missing $missing ACE(s)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
113
ad-objects/lib/ADGroup.ps1
Normal file
113
ad-objects/lib/ADGroup.ps1
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# ADGroup.ps1
|
||||||
|
# Security group management.
|
||||||
|
# No dependencies on other AD modules.
|
||||||
|
|
||||||
|
function Ensure-ADSecurityGroup {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Idempotently creates a security group and syncs membership.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Name,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Path,
|
||||||
|
|
||||||
|
[string]$Description = '',
|
||||||
|
|
||||||
|
[ValidateSet('Global', 'DomainLocal', 'Universal')]
|
||||||
|
[string]$Scope = 'Global',
|
||||||
|
|
||||||
|
[string[]]$Members = @()
|
||||||
|
)
|
||||||
|
|
||||||
|
$created = $false
|
||||||
|
|
||||||
|
try {
|
||||||
|
$group = Get-ADGroup -Identity $Name -ErrorAction Stop
|
||||||
|
Write-Host " [OK] Group exists: $Name" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
New-ADGroup -Name $Name -Path $Path -GroupScope $Scope -GroupCategory Security `
|
||||||
|
-Description $Description -ProtectedFromAccidentalDeletion $true
|
||||||
|
Write-Host " [CREATED] Group: $Name" -ForegroundColor Yellow
|
||||||
|
$created = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure protected from accidental deletion
|
||||||
|
$group = Get-ADGroup -Identity $Name -Properties ProtectedFromAccidentalDeletion
|
||||||
|
if (-not $group.ProtectedFromAccidentalDeletion) {
|
||||||
|
Set-ADObject -Identity $group.DistinguishedName -ProtectedFromAccidentalDeletion $true
|
||||||
|
Write-Host " [UPDATED] $Name ProtectedFromAccidentalDeletion=True" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sync membership
|
||||||
|
if ($Members.Count -gt 0) {
|
||||||
|
$currentMembers = @(Get-ADGroupMember -Identity $Name -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SamAccountName)
|
||||||
|
|
||||||
|
foreach ($member in $Members) {
|
||||||
|
if ($member -notin $currentMembers) {
|
||||||
|
Add-ADGroupMember -Identity $Name -Members $member
|
||||||
|
Write-Host " [ADDED] $member -> $Name" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove members not in desired list
|
||||||
|
foreach ($current in $currentMembers) {
|
||||||
|
if ($current -notin $Members) {
|
||||||
|
Remove-ADGroupMember -Identity $Name -Members $current -Confirm:$false
|
||||||
|
Write-Host " [REMOVED] $current from $Name" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $created
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-ADSecurityGroup {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compares desired group state against AD. Returns diffs.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Name,
|
||||||
|
|
||||||
|
[string[]]$Members = @()
|
||||||
|
)
|
||||||
|
|
||||||
|
$diffs = @()
|
||||||
|
|
||||||
|
try {
|
||||||
|
$group = Get-ADGroup -Identity $Name -Properties ProtectedFromAccidentalDeletion -ErrorAction Stop
|
||||||
|
Write-Host " [OK] Group exists: $Name" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Check protection
|
||||||
|
if (-not $group.ProtectedFromAccidentalDeletion) {
|
||||||
|
Write-Host " [DRIFT] $Name not protected from accidental deletion" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{ Type = 'GroupProtection'; Group = $Name; Status = 'Unprotected' }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check membership
|
||||||
|
$currentMembers = @(Get-ADGroupMember -Identity $Name -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SamAccountName)
|
||||||
|
|
||||||
|
foreach ($member in $Members) {
|
||||||
|
if ($member -notin $currentMembers) {
|
||||||
|
Write-Host " [DRIFT] $member not in group $Name" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{ Type = 'GroupMember'; Group = $Name; Member = $member; Status = 'Missing' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($current in $currentMembers) {
|
||||||
|
if ($current -notin $Members) {
|
||||||
|
Write-Host " [DRIFT] $current in group $Name but not in desired state" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{ Type = 'GroupMember'; Group = $Name; Member = $current; Status = 'Extra' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host " [MISSING] Group: $Name" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{ Type = 'Group'; Name = $Name; Status = 'Missing' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
15
ad-objects/lib/ADHelper.ps1
Normal file
15
ad-objects/lib/ADHelper.ps1
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# ADHelper.ps1
|
||||||
|
# Loader: dot-sources the modular AD helper library.
|
||||||
|
# Apply-ADBaseline.ps1 continues to load this single file -- zero breaking changes.
|
||||||
|
|
||||||
|
$libDir = $PSScriptRoot
|
||||||
|
|
||||||
|
# Core utilities (other modules depend on these)
|
||||||
|
. (Join-Path $libDir 'ADCore.ps1')
|
||||||
|
|
||||||
|
# Feature modules (order-independent)
|
||||||
|
. (Join-Path $libDir 'ADOrganizationalUnit.ps1')
|
||||||
|
. (Join-Path $libDir 'ADGroup.ps1')
|
||||||
|
. (Join-Path $libDir 'ADUser.ps1')
|
||||||
|
. (Join-Path $libDir 'ADDelegation.ps1')
|
||||||
|
. (Join-Path $libDir 'ADPasswordPolicy.ps1')
|
||||||
56
ad-objects/lib/ADOrganizationalUnit.ps1
Normal file
56
ad-objects/lib/ADOrganizationalUnit.ps1
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# ADOrganizationalUnit.ps1
|
||||||
|
# Organizational Unit management.
|
||||||
|
# No dependencies on other AD modules.
|
||||||
|
|
||||||
|
function Ensure-ADOU {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Idempotently creates an OU. Returns $true if created, $false if already exists.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Name,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Path,
|
||||||
|
|
||||||
|
[string]$Description = ''
|
||||||
|
)
|
||||||
|
|
||||||
|
$dn = "OU=$Name,$Path"
|
||||||
|
|
||||||
|
try {
|
||||||
|
Get-ADOrganizationalUnit -Identity $dn -ErrorAction Stop | Out-Null
|
||||||
|
Write-Host " [OK] OU exists: $Name" -ForegroundColor Green
|
||||||
|
return $false
|
||||||
|
} catch {
|
||||||
|
New-ADOrganizationalUnit -Name $Name -Path $Path -Description $Description -ProtectedFromAccidentalDeletion $true
|
||||||
|
Write-Host " [CREATED] OU: $Name ($dn)" -ForegroundColor Yellow
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-ADOU {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Checks if an OU exists. Returns a diff object if missing.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Name,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Path
|
||||||
|
)
|
||||||
|
|
||||||
|
$dn = "OU=$Name,$Path"
|
||||||
|
|
||||||
|
try {
|
||||||
|
Get-ADOrganizationalUnit -Identity $dn -ErrorAction Stop | Out-Null
|
||||||
|
Write-Host " [OK] OU exists: $Name" -ForegroundColor Green
|
||||||
|
return $null
|
||||||
|
} catch {
|
||||||
|
Write-Host " [MISSING] OU: $Name ($dn)" -ForegroundColor Red
|
||||||
|
return [PSCustomObject]@{ Type = 'OU'; Name = $Name; Status = 'Missing' }
|
||||||
|
}
|
||||||
|
}
|
||||||
224
ad-objects/lib/ADPasswordPolicy.ps1
Normal file
224
ad-objects/lib/ADPasswordPolicy.ps1
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
# ADPasswordPolicy.ps1
|
||||||
|
# Fine-grained password policy (PSO) management.
|
||||||
|
# No dependencies on other AD modules.
|
||||||
|
|
||||||
|
function Ensure-ADPasswordPolicy {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Idempotently creates or updates a fine-grained password policy (PSO)
|
||||||
|
and syncs group linkage via AppliesTo.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$Definition
|
||||||
|
)
|
||||||
|
|
||||||
|
$name = $Definition.Name
|
||||||
|
|
||||||
|
# Separate AppliesTo from PSO properties
|
||||||
|
$appliesTo = @()
|
||||||
|
if ($Definition.ContainsKey('AppliesTo')) {
|
||||||
|
$appliesTo = @($Definition.AppliesTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pso = Get-ADFineGrainedPasswordPolicy -Identity $name -Properties Description -ErrorAction Stop
|
||||||
|
Write-Host " [OK] PSO exists: $name" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Check each property for drift
|
||||||
|
$propMap = @{
|
||||||
|
'Precedence' = 'Precedence'
|
||||||
|
'MinPasswordLength' = 'MinPasswordLength'
|
||||||
|
'PasswordHistoryCount' = 'PasswordHistoryCount'
|
||||||
|
'MaxPasswordAge' = 'MaxPasswordAge'
|
||||||
|
'MinPasswordAge' = 'MinPasswordAge'
|
||||||
|
'ComplexityEnabled' = 'ComplexityEnabled'
|
||||||
|
'ReversibleEncryptionEnabled' = 'ReversibleEncryptionEnabled'
|
||||||
|
'LockoutThreshold' = 'LockoutThreshold'
|
||||||
|
'LockoutDuration' = 'LockoutDuration'
|
||||||
|
'LockoutObservationWindow' = 'LockoutObservationWindow'
|
||||||
|
}
|
||||||
|
|
||||||
|
$updates = @{}
|
||||||
|
foreach ($key in $propMap.Keys) {
|
||||||
|
if ($Definition.ContainsKey($key)) {
|
||||||
|
$desired = $Definition[$key]
|
||||||
|
$actual = $pso.$($propMap[$key])
|
||||||
|
|
||||||
|
# Normalize TimeSpan comparisons
|
||||||
|
if ($desired -is [string] -and $actual -is [timespan]) {
|
||||||
|
$desiredTS = [timespan]::Parse($desired)
|
||||||
|
if ($actual -ne $desiredTS) {
|
||||||
|
$updates[$key] = $desired
|
||||||
|
}
|
||||||
|
} elseif ("$actual" -ne "$desired") {
|
||||||
|
$updates[$key] = $desired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Definition.ContainsKey('Description')) {
|
||||||
|
if ("$($pso.Description)" -ne "$($Definition.Description)") {
|
||||||
|
$updates['Description'] = $Definition.Description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($updates.Count -gt 0) {
|
||||||
|
Set-ADFineGrainedPasswordPolicy -Identity $name @updates
|
||||||
|
foreach ($key in $updates.Keys) {
|
||||||
|
Write-Host " [UPDATED] $name $key='$($updates[$key])'" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
|
||||||
|
# Build creation parameters
|
||||||
|
$createParams = @{
|
||||||
|
Name = $name
|
||||||
|
Precedence = $Definition.Precedence
|
||||||
|
}
|
||||||
|
|
||||||
|
# Map all supported properties
|
||||||
|
$createKeys = @(
|
||||||
|
'Description', 'MinPasswordLength', 'PasswordHistoryCount',
|
||||||
|
'MaxPasswordAge', 'MinPasswordAge', 'ComplexityEnabled',
|
||||||
|
'ReversibleEncryptionEnabled', 'LockoutThreshold',
|
||||||
|
'LockoutDuration', 'LockoutObservationWindow'
|
||||||
|
)
|
||||||
|
foreach ($key in $createKeys) {
|
||||||
|
if ($Definition.ContainsKey($key)) {
|
||||||
|
$createParams[$key] = $Definition[$key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ProtectedFromAccidentalDeletion for consistency
|
||||||
|
$createParams['ProtectedFromAccidentalDeletion'] = $true
|
||||||
|
|
||||||
|
New-ADFineGrainedPasswordPolicy @createParams
|
||||||
|
Write-Host " [CREATED] PSO: $name (precedence $($Definition.Precedence))" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sync AppliesTo group linkage
|
||||||
|
if ($appliesTo.Count -gt 0) {
|
||||||
|
$pso = Get-ADFineGrainedPasswordPolicy -Identity $name -Properties AppliesTo
|
||||||
|
$currentSubjects = @()
|
||||||
|
if ($pso.AppliesTo) {
|
||||||
|
$currentSubjects = @($pso.AppliesTo | ForEach-Object {
|
||||||
|
(Get-ADGroup -Identity $_ -ErrorAction SilentlyContinue).SamAccountName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($group in $appliesTo) {
|
||||||
|
if ($group -notin $currentSubjects) {
|
||||||
|
Add-ADFineGrainedPasswordPolicySubject -Identity $name -Subjects $group
|
||||||
|
Write-Host " [LINKED] $name -> $group" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($current in $currentSubjects) {
|
||||||
|
if ($current -notin $appliesTo) {
|
||||||
|
Remove-ADFineGrainedPasswordPolicySubject -Identity $name -Subjects $current -Confirm:$false
|
||||||
|
Write-Host " [UNLINKED] $name -> $current" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-ADPasswordPolicy {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compares desired PSO state against AD. Returns diffs.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$Definition
|
||||||
|
)
|
||||||
|
|
||||||
|
$name = $Definition.Name
|
||||||
|
$diffs = @()
|
||||||
|
|
||||||
|
# Separate AppliesTo from PSO properties
|
||||||
|
$appliesTo = @()
|
||||||
|
if ($Definition.ContainsKey('AppliesTo')) {
|
||||||
|
$appliesTo = @($Definition.AppliesTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pso = Get-ADFineGrainedPasswordPolicy -Identity $name -Properties AppliesTo, Description -ErrorAction Stop
|
||||||
|
Write-Host " [OK] PSO exists: $name" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Check each property
|
||||||
|
$propMap = @{
|
||||||
|
'Precedence' = 'Precedence'
|
||||||
|
'MinPasswordLength' = 'MinPasswordLength'
|
||||||
|
'PasswordHistoryCount' = 'PasswordHistoryCount'
|
||||||
|
'MaxPasswordAge' = 'MaxPasswordAge'
|
||||||
|
'MinPasswordAge' = 'MinPasswordAge'
|
||||||
|
'ComplexityEnabled' = 'ComplexityEnabled'
|
||||||
|
'ReversibleEncryptionEnabled' = 'ReversibleEncryptionEnabled'
|
||||||
|
'LockoutThreshold' = 'LockoutThreshold'
|
||||||
|
'LockoutDuration' = 'LockoutDuration'
|
||||||
|
'LockoutObservationWindow' = 'LockoutObservationWindow'
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($key in $propMap.Keys) {
|
||||||
|
if ($Definition.ContainsKey($key)) {
|
||||||
|
$desired = $Definition[$key]
|
||||||
|
$actual = $pso.$($propMap[$key])
|
||||||
|
|
||||||
|
$isDrift = $false
|
||||||
|
if ($desired -is [string] -and $actual -is [timespan]) {
|
||||||
|
$desiredTS = [timespan]::Parse($desired)
|
||||||
|
if ($actual -ne $desiredTS) { $isDrift = $true }
|
||||||
|
} elseif ("$actual" -ne "$desired") {
|
||||||
|
$isDrift = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isDrift) {
|
||||||
|
Write-Host " [DRIFT] $name $key='$actual', expected '$desired'" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'PSOProperty'; PSO = $name
|
||||||
|
Property = $key; Current = $actual; Desired = $desired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Definition.ContainsKey('Description')) {
|
||||||
|
if ("$($pso.Description)" -ne "$($Definition.Description)") {
|
||||||
|
Write-Host " [DRIFT] $name Description='$($pso.Description)', expected '$($Definition.Description)'" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'PSOProperty'; PSO = $name
|
||||||
|
Property = 'Description'; Current = $pso.Description; Desired = $Definition.Description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check AppliesTo linkage
|
||||||
|
$currentSubjects = @()
|
||||||
|
if ($pso.AppliesTo) {
|
||||||
|
$currentSubjects = @($pso.AppliesTo | ForEach-Object {
|
||||||
|
(Get-ADGroup -Identity $_ -ErrorAction SilentlyContinue).SamAccountName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($group in $appliesTo) {
|
||||||
|
if ($group -notin $currentSubjects) {
|
||||||
|
Write-Host " [DRIFT] $name not linked to $group" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{ Type = 'PSOSubject'; PSO = $name; Group = $group; Status = 'Missing' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($current in $currentSubjects) {
|
||||||
|
if ($current -notin $appliesTo) {
|
||||||
|
Write-Host " [DRIFT] $name linked to $current but not in desired state" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{ Type = 'PSOSubject'; PSO = $name; Group = $current; Status = 'Extra' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
|
||||||
|
Write-Host " [MISSING] PSO: $name" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{ Type = 'PSO'; Name = $name; Status = 'Missing' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
200
ad-objects/lib/ADUser.ps1
Normal file
200
ad-objects/lib/ADUser.ps1
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
# ADUser.ps1
|
||||||
|
# User account management.
|
||||||
|
# Depends on: ADCore.ps1 (New-RandomPassword)
|
||||||
|
|
||||||
|
function Ensure-ADUserAccount {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Idempotently creates a user account, ensures correct OU placement and group membership.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$SamAccountName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Name,
|
||||||
|
|
||||||
|
[string]$GivenName = '',
|
||||||
|
[string]$Surname = '',
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Path,
|
||||||
|
|
||||||
|
[bool]$Enabled = $true,
|
||||||
|
|
||||||
|
[string[]]$MemberOf = @(),
|
||||||
|
|
||||||
|
[hashtable]$Properties = @{}
|
||||||
|
)
|
||||||
|
|
||||||
|
$created = $false
|
||||||
|
|
||||||
|
try {
|
||||||
|
$user = Get-ADUser -Identity $SamAccountName -Properties MemberOf -ErrorAction Stop
|
||||||
|
Write-Host " [OK] User exists: $SamAccountName" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Check OU placement
|
||||||
|
$currentOU = ($user.DistinguishedName -split ',', 2)[1]
|
||||||
|
if ($currentOU -ne $Path) {
|
||||||
|
Move-ADObject -Identity $user.DistinguishedName -TargetPath $Path
|
||||||
|
Write-Host " [MOVED] $SamAccountName -> $Path" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check enabled state
|
||||||
|
if ($user.Enabled -ne $Enabled) {
|
||||||
|
Set-ADUser -Identity $SamAccountName -Enabled $Enabled
|
||||||
|
Write-Host " [UPDATED] $SamAccountName Enabled=$Enabled" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check optional properties
|
||||||
|
if ($Properties.Count -gt 0) {
|
||||||
|
$propNames = @($Properties.Keys)
|
||||||
|
$current = Get-ADUser -Identity $SamAccountName -Properties $propNames
|
||||||
|
foreach ($prop in $propNames) {
|
||||||
|
$desired = $Properties[$prop]
|
||||||
|
$actual = $current.$prop
|
||||||
|
if ("$actual" -ne "$desired") {
|
||||||
|
Set-ADUser -Identity $SamAccountName -Replace @{ $prop = $desired }
|
||||||
|
Write-Host " [UPDATED] $SamAccountName $prop='$desired'" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
|
||||||
|
$password = New-RandomPassword
|
||||||
|
$securePass = ConvertTo-SecureString $password -AsPlainText -Force
|
||||||
|
|
||||||
|
$params = @{
|
||||||
|
SamAccountName = $SamAccountName
|
||||||
|
Name = $Name
|
||||||
|
UserPrincipalName = "$SamAccountName@$((Get-ADDomain).DNSRoot)"
|
||||||
|
Path = $Path
|
||||||
|
AccountPassword = $securePass
|
||||||
|
Enabled = $Enabled
|
||||||
|
ChangePasswordAtLogon = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($GivenName) { $params.GivenName = $GivenName }
|
||||||
|
if ($Surname) { $params.Surname = $Surname }
|
||||||
|
|
||||||
|
# Merge optional properties into creation parameters
|
||||||
|
foreach ($key in $Properties.Keys) {
|
||||||
|
$params[$key] = $Properties[$key]
|
||||||
|
}
|
||||||
|
|
||||||
|
New-ADUser @params
|
||||||
|
|
||||||
|
# Write password to a secured file -- never to console (avoids transcript leaks)
|
||||||
|
$credDir = Join-Path (Split-Path $PSScriptRoot -Parent) '.credentials'
|
||||||
|
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||||
|
|
||||||
|
if (-not (Test-Path $credDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $credDir -Force | Out-Null
|
||||||
|
|
||||||
|
# Lock directory permissions to current user only
|
||||||
|
$dirAcl = Get-Acl $credDir
|
||||||
|
$dirAcl.SetAccessRuleProtection($true, $false)
|
||||||
|
$dirRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
||||||
|
$currentUser, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
|
||||||
|
$dirAcl.AddAccessRule($dirRule)
|
||||||
|
Set-Acl -Path $credDir -AclObject $dirAcl
|
||||||
|
}
|
||||||
|
|
||||||
|
$credFile = Join-Path $credDir "$SamAccountName.txt"
|
||||||
|
Set-Content -Path $credFile -Value "User: $SamAccountName`nPassword: $password`nCreated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')`nNote: Must change on first login`nDelete this file after handing off the password."
|
||||||
|
|
||||||
|
# Lock file permissions to current user only
|
||||||
|
$acl = Get-Acl $credFile
|
||||||
|
$acl.SetAccessRuleProtection($true, $false)
|
||||||
|
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
||||||
|
$currentUser, 'FullControl', 'Allow')
|
||||||
|
$acl.AddAccessRule($rule)
|
||||||
|
Set-Acl -Path $credFile -AclObject $acl
|
||||||
|
|
||||||
|
Write-Host " [CREATED] User: $SamAccountName (password saved to $credFile)" -ForegroundColor Yellow
|
||||||
|
$created = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sync group membership
|
||||||
|
foreach ($group in $MemberOf) {
|
||||||
|
$currentGroups = @(Get-ADPrincipalGroupMembership -Identity $SamAccountName | Select-Object -ExpandProperty Name)
|
||||||
|
if ($group -notin $currentGroups) {
|
||||||
|
Add-ADGroupMember -Identity $group -Members $SamAccountName
|
||||||
|
Write-Host " [ADDED] $SamAccountName -> $group" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $created
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-ADUserAccount {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compares desired user state against AD. Returns diffs.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$SamAccountName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Path,
|
||||||
|
|
||||||
|
[bool]$Enabled = $true,
|
||||||
|
|
||||||
|
[string[]]$MemberOf = @(),
|
||||||
|
|
||||||
|
[hashtable]$Properties = @{}
|
||||||
|
)
|
||||||
|
|
||||||
|
$diffs = @()
|
||||||
|
|
||||||
|
try {
|
||||||
|
$user = Get-ADUser -Identity $SamAccountName -Properties MemberOf -ErrorAction Stop
|
||||||
|
Write-Host " [OK] User exists: $SamAccountName" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Check OU placement
|
||||||
|
$currentOU = ($user.DistinguishedName -split ',', 2)[1]
|
||||||
|
if ($currentOU -ne $Path) {
|
||||||
|
Write-Host " [DRIFT] $SamAccountName in $currentOU, expected $Path" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{ Type = 'UserOU'; User = $SamAccountName; Current = $currentOU; Desired = $Path }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check enabled state
|
||||||
|
if ($user.Enabled -ne $Enabled) {
|
||||||
|
Write-Host " [DRIFT] $SamAccountName Enabled=$($user.Enabled), expected $Enabled" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{ Type = 'UserEnabled'; User = $SamAccountName; Current = $user.Enabled; Desired = $Enabled }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check optional properties
|
||||||
|
if ($Properties.Count -gt 0) {
|
||||||
|
$propNames = @($Properties.Keys)
|
||||||
|
$current = Get-ADUser -Identity $SamAccountName -Properties $propNames
|
||||||
|
foreach ($prop in $propNames) {
|
||||||
|
$desired = $Properties[$prop]
|
||||||
|
$actual = $current.$prop
|
||||||
|
if ("$actual" -ne "$desired") {
|
||||||
|
Write-Host " [DRIFT] $SamAccountName $prop='$actual', expected '$desired'" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'UserProperty'; User = $SamAccountName
|
||||||
|
Property = $prop; Current = $actual; Desired = $desired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check group membership
|
||||||
|
$currentGroups = @(Get-ADPrincipalGroupMembership -Identity $SamAccountName | Select-Object -ExpandProperty Name)
|
||||||
|
foreach ($group in $MemberOf) {
|
||||||
|
if ($group -notin $currentGroups) {
|
||||||
|
Write-Host " [DRIFT] $SamAccountName not in group $group" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{ Type = 'UserGroup'; User = $SamAccountName; Group = $group; Status = 'Missing' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
|
||||||
|
Write-Host " [MISSING] User: $SamAccountName" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{ Type = 'User'; Name = $SamAccountName; Status = 'Missing' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
32
ad-objects/ous.ps1
Normal file
32
ad-objects/ous.ps1
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Organizational Unit Definitions
|
||||||
|
# Processed first — OUs must exist before groups and users can be placed in them.
|
||||||
|
|
||||||
|
$domainDN = 'DC=example,DC=internal'
|
||||||
|
|
||||||
|
@(
|
||||||
|
@{
|
||||||
|
Name = 'ExampleUsers'
|
||||||
|
Path = $domainDN
|
||||||
|
Description = 'Standard user accounts'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Name = 'ExampleWorkstations'
|
||||||
|
Path = $domainDN
|
||||||
|
Description = 'Domain-joined workstations'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Name = 'ExampleServers'
|
||||||
|
Path = $domainDN
|
||||||
|
Description = 'Domain-joined servers'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Name = 'ExampleAdmins'
|
||||||
|
Path = $domainDN
|
||||||
|
Description = 'Delegated administrator accounts'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Name = 'ExampleAdminWorkstations'
|
||||||
|
Path = $domainDN
|
||||||
|
Description = 'Privileged access workstations for admin accounts'
|
||||||
|
}
|
||||||
|
)
|
||||||
37
ad-objects/password-policies.ps1
Normal file
37
ad-objects/password-policies.ps1
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Fine-Grained Password Policy Definitions (PSOs)
|
||||||
|
# Override Default Domain Policy for specific groups.
|
||||||
|
# Lower Precedence number = higher priority.
|
||||||
|
|
||||||
|
@(
|
||||||
|
@{
|
||||||
|
Name = 'PSO-MasterAdmins'
|
||||||
|
Description = 'Strict password policy for Tier 0 admin accounts'
|
||||||
|
Precedence = 10
|
||||||
|
MinPasswordLength = 16
|
||||||
|
PasswordHistoryCount = 48
|
||||||
|
MaxPasswordAge = '30.00:00:00'
|
||||||
|
MinPasswordAge = '1.00:00:00'
|
||||||
|
ComplexityEnabled = $true
|
||||||
|
ReversibleEncryptionEnabled = $false
|
||||||
|
LockoutThreshold = 3
|
||||||
|
LockoutDuration = '00:30:00'
|
||||||
|
LockoutObservationWindow = '00:30:00'
|
||||||
|
AppliesTo = @('MasterAdmins')
|
||||||
|
}
|
||||||
|
|
||||||
|
@{
|
||||||
|
Name = 'PSO-DelegatedAdmins'
|
||||||
|
Description = 'Moderate password policy for helpdesk admins'
|
||||||
|
Precedence = 20
|
||||||
|
MinPasswordLength = 12
|
||||||
|
PasswordHistoryCount = 24
|
||||||
|
MaxPasswordAge = '42.00:00:00'
|
||||||
|
MinPasswordAge = '1.00:00:00'
|
||||||
|
ComplexityEnabled = $true
|
||||||
|
ReversibleEncryptionEnabled = $false
|
||||||
|
LockoutThreshold = 5
|
||||||
|
LockoutDuration = '00:30:00'
|
||||||
|
LockoutObservationWindow = '00:30:00'
|
||||||
|
AppliesTo = @('DelegatedAdmins')
|
||||||
|
}
|
||||||
|
)
|
||||||
47
ad-objects/users.ps1
Normal file
47
ad-objects/users.ps1
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# User Account Definitions
|
||||||
|
# Processed after OUs and groups.
|
||||||
|
# New users get a CSPRNG password saved to ad-objects/.credentials/ (never printed to console).
|
||||||
|
|
||||||
|
@(
|
||||||
|
# --- Master Admins (ExampleAdmins OU) ---
|
||||||
|
|
||||||
|
@{
|
||||||
|
SamAccountName = 't0admin'
|
||||||
|
Name = 'Tier Zero Admin'
|
||||||
|
GivenName = 'Tier'
|
||||||
|
Surname = 'Admin'
|
||||||
|
Path = 'OU=ExampleAdmins,DC=example,DC=internal'
|
||||||
|
Enabled = $true
|
||||||
|
MemberOf = @('MasterAdmins', 'Administrators', 'Group Policy Creator Owners', 'DnsAdmins', 'Remote Desktop Users')
|
||||||
|
Description = 'Tier 0 master admin -- GPO/AD management via RDP to DC01'
|
||||||
|
Title = 'Master Administrator'
|
||||||
|
Department = 'IT'
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Delegated Admins (ExampleUsers OU) ---
|
||||||
|
|
||||||
|
@{
|
||||||
|
SamAccountName = 'jsmith'
|
||||||
|
Name = 'John Smith'
|
||||||
|
GivenName = 'John'
|
||||||
|
Surname = 'Smith'
|
||||||
|
Path = 'OU=ExampleUsers,DC=example,DC=internal'
|
||||||
|
Enabled = $true
|
||||||
|
MemberOf = @('DelegatedAdmins')
|
||||||
|
Description = 'Delegated admin -- helpdesk via RSAT on WS01'
|
||||||
|
Title = 'Systems Administrator'
|
||||||
|
Department = 'IT'
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Standard Users (ExampleUsers OU) ---
|
||||||
|
|
||||||
|
@{
|
||||||
|
SamAccountName = 'jdoe'
|
||||||
|
Name = 'Jane Doe'
|
||||||
|
GivenName = 'Jane'
|
||||||
|
Surname = 'Doe'
|
||||||
|
Path = 'OU=ExampleUsers,DC=example,DC=internal'
|
||||||
|
Enabled = $true
|
||||||
|
MemberOf = @()
|
||||||
|
}
|
||||||
|
)
|
||||||
92
gpo/Apply-DSCBaseline.ps1
Normal file
92
gpo/Apply-DSCBaseline.ps1
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# Apply-DscBaseline.ps1
|
||||||
|
# DC-local compliance validation using DSC.
|
||||||
|
#
|
||||||
|
# Compiles DSC configurations from settings.ps1 (same source of truth as
|
||||||
|
# Apply-GPOBaseline.ps1), then tests local state against the compiled MOF.
|
||||||
|
# This script is test-only by design — it never modifies the system.
|
||||||
|
# To fix drift, edit settings.ps1 and run Apply-GPOBaseline.ps1.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# .\Apply-DscBaseline.ps1 # Check DC compliance (read-only)
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param()
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$ScriptRoot = $PSScriptRoot
|
||||||
|
$OutputPath = Join-Path $ScriptRoot 'Output'
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Load shared helper functions (needed by DSC config files)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
. (Join-Path $ScriptRoot 'lib\GPOHelper.ps1')
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Step 1: Ensure SecurityPolicyDsc module is installed
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
Write-Host 'Checking for SecurityPolicyDsc module...' -ForegroundColor Cyan
|
||||||
|
if (-not (Get-Module -ListAvailable -Name SecurityPolicyDsc)) {
|
||||||
|
Write-Host 'Installing SecurityPolicyDsc from PSGallery...' -ForegroundColor Yellow
|
||||||
|
Install-Module -Name SecurityPolicyDsc -Repository PSGallery -Force -Scope AllUsers
|
||||||
|
Write-Host 'Installed.' -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host 'SecurityPolicyDsc is already installed.' -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Step 2: Create output directory for compiled MOF files
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
if (-not (Test-Path $OutputPath)) {
|
||||||
|
New-Item -ItemType Directory -Path $OutputPath | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Step 3: Load and compile configurations
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
$configurations = @(
|
||||||
|
@{
|
||||||
|
Name = 'DefaultDomainPolicy'
|
||||||
|
Script = Join-Path $ScriptRoot 'default-domain\DefaultDomainPolicy.ps1'
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Name = 'DefaultDCPolicy'
|
||||||
|
Script = Join-Path $ScriptRoot 'default-domain-controller\DefaultDCPolicy.ps1'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($config in $configurations) {
|
||||||
|
Write-Host "`nLoading $($config.Name)..." -ForegroundColor Cyan
|
||||||
|
. $config.Script
|
||||||
|
|
||||||
|
$mofOutput = Join-Path $OutputPath $config.Name
|
||||||
|
Write-Host "Compiling $($config.Name) -> $mofOutput" -ForegroundColor Cyan
|
||||||
|
& $config.Name -OutputPath $mofOutput | Out-Null
|
||||||
|
Write-Host "Compiled." -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Step 4: Test compliance
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
foreach ($config in $configurations) {
|
||||||
|
$mofPath = Join-Path $OutputPath $config.Name
|
||||||
|
|
||||||
|
Write-Host "`n--- Testing compliance: $($config.Name) ---" -ForegroundColor Yellow
|
||||||
|
$result = Test-DscConfiguration -ReferenceConfiguration (Join-Path $mofPath 'localhost.mof')
|
||||||
|
if ($result.InDesiredState) {
|
||||||
|
Write-Host " [OK] All $($result.ResourcesInDesiredState.Count) resource(s) in desired state" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [DRIFT] $($result.ResourcesNotInDesiredState.Count) resource(s) out of desired state:" -ForegroundColor Red
|
||||||
|
foreach ($resource in $result.ResourcesNotInDesiredState) {
|
||||||
|
Write-Host " $($resource.ResourceId)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($result.ResourcesInDesiredState.Count -gt 0) {
|
||||||
|
Write-Host " [OK] $($result.ResourcesInDesiredState.Count) resource(s) in desired state" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Step 5: Summary
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
Write-Host "`nCompliance test complete. No changes were made." -ForegroundColor Yellow
|
||||||
|
Write-Host 'To fix drift, edit settings.ps1 and run Apply-GPOBaseline.ps1.' -ForegroundColor DarkGray
|
||||||
376
gpo/Apply-GPOBaseline.ps1
Normal file
376
gpo/Apply-GPOBaseline.ps1
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
# Apply-GPOBaseline.ps1
|
||||||
|
# Orchestration script for applying GPO settings to Active Directory.
|
||||||
|
#
|
||||||
|
# Reads settings.ps1 from each GPO directory, compares against the live
|
||||||
|
# GPO in AD, and applies changes. Works from any machine with RSAT.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# .\Apply-GPOBaseline.ps1 # Apply all GPO settings
|
||||||
|
# .\Apply-GPOBaseline.ps1 -TestOnly # Compare desired vs. current (no changes)
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[switch]$TestOnly,
|
||||||
|
[switch]$GpUpdate,
|
||||||
|
[switch]$NoBackup,
|
||||||
|
[switch]$NoCleanup
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$ScriptRoot = $PSScriptRoot
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Load helper functions
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
. (Join-Path $ScriptRoot 'lib\GPOHelper.ps1')
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Groups that should have edit rights on ALL managed GPOs.
|
||||||
|
# This ensures Tier 0 operational admins can manage GPOs without
|
||||||
|
# Domain Admins membership (DA = break-glass only).
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
$ManagementGroups = @('MasterAdmins')
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Discover GPO directories (each must contain a settings.ps1)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
$gpoDirs = Get-ChildItem -Path $ScriptRoot -Directory |
|
||||||
|
Where-Object { Test-Path (Join-Path $_.FullName 'settings.ps1') }
|
||||||
|
|
||||||
|
if ($gpoDirs.Count -eq 0) {
|
||||||
|
Write-Host 'No GPO directories with settings.ps1 found.' -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Found $($gpoDirs.Count) GPO configuration(s):`n" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$totalDiffs = 0
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Process each GPO
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
foreach ($dir in $gpoDirs) {
|
||||||
|
$settingsPath = Join-Path $dir.FullName 'settings.ps1'
|
||||||
|
$settings = & $settingsPath
|
||||||
|
|
||||||
|
$gpoName = $settings.GPOName
|
||||||
|
Write-Host "=== $gpoName ===" -ForegroundColor White
|
||||||
|
|
||||||
|
# Verify GPO exists in AD — create if missing
|
||||||
|
try {
|
||||||
|
$gpo = Get-GPO -Name $gpoName -ErrorAction Stop
|
||||||
|
Write-Host " GPO found: {$($gpo.Id)}" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
if ($TestOnly) {
|
||||||
|
Write-Host " [DRIFT] GPO '$gpoName' does not exist yet (will be created on apply)" -ForegroundColor Yellow
|
||||||
|
$totalDiffs++
|
||||||
|
Write-Host ''
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
Write-Host " GPO '$gpoName' not found - creating..." -ForegroundColor Yellow
|
||||||
|
$newGpoParams = @{ Name = $gpoName }
|
||||||
|
if ($settings.Description) { $newGpoParams.Comment = $settings.Description }
|
||||||
|
$gpo = New-GPO @newGpoParams
|
||||||
|
Write-Host " [CREATED] GPO: $gpoName {$($gpo.Id)}" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- GPO Description ---
|
||||||
|
if ($settings.Description) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
if ($gpo.Description -ne $settings.Description) {
|
||||||
|
Write-Host " [DRIFT] Description: '$($gpo.Description)' -> '$($settings.Description)'" -ForegroundColor Red
|
||||||
|
$totalDiffs++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($gpo.Description -ne $settings.Description) {
|
||||||
|
$gpo.Description = $settings.Description
|
||||||
|
Write-Host " [UPDATED] Description set" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- GPO Status (enabled/disabled sections) ---
|
||||||
|
$hasStatusConfig = $settings.ContainsKey('DisableUserConfiguration') -or
|
||||||
|
$settings.ContainsKey('DisableComputerConfiguration')
|
||||||
|
if ($hasStatusConfig) {
|
||||||
|
$disableUser = if ($settings.ContainsKey('DisableUserConfiguration')) {
|
||||||
|
$settings.DisableUserConfiguration
|
||||||
|
} else { $false }
|
||||||
|
$disableComputer = if ($settings.ContainsKey('DisableComputerConfiguration')) {
|
||||||
|
$settings.DisableComputerConfiguration
|
||||||
|
} else { $false }
|
||||||
|
|
||||||
|
if ($TestOnly) {
|
||||||
|
$statusDiff = Compare-GPOStatus -GPOName $gpoName `
|
||||||
|
-DisableUserConfiguration $disableUser `
|
||||||
|
-DisableComputerConfiguration $disableComputer
|
||||||
|
if ($statusDiff) { $totalDiffs++ }
|
||||||
|
} else {
|
||||||
|
Ensure-GPOStatus -GPOName $gpoName `
|
||||||
|
-DisableUserConfiguration $disableUser `
|
||||||
|
-DisableComputerConfiguration $disableComputer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Pre-Apply Backup ---
|
||||||
|
if (-not $TestOnly -and -not $NoBackup) {
|
||||||
|
try {
|
||||||
|
Backup-GPOState -GPOName $gpoName | Out-Null
|
||||||
|
} catch {
|
||||||
|
Write-Host " [WARN] Backup failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||||
|
Write-Host " Continuing with apply..." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Management Permissions ---
|
||||||
|
foreach ($mgmtGroup in $ManagementGroups) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
$permDiff = Compare-GPOManagementPermission -GPOName $gpoName -GroupName $mgmtGroup
|
||||||
|
if ($permDiff) { $totalDiffs++ }
|
||||||
|
} else {
|
||||||
|
Ensure-GPOManagementPermission -GPOName $gpoName -GroupName $mgmtGroup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Version scope tracking (for deferred bump at end of GPO) ---
|
||||||
|
$bumpMachine = $false
|
||||||
|
$bumpUser = $false
|
||||||
|
|
||||||
|
# --- Restricted Groups -> merge into SecurityPolicy ---
|
||||||
|
if ($settings.RestrictedGroups -and $settings.RestrictedGroups.Count -gt 0) {
|
||||||
|
if ($settings.SecurityPolicy -and $settings.SecurityPolicy.ContainsKey('Group Membership')) {
|
||||||
|
throw "GPO '$gpoName': Use RestrictedGroups OR SecurityPolicy['Group Membership'], not both."
|
||||||
|
}
|
||||||
|
$gmEntries = ConvertTo-RestrictedGroupEntries -RestrictedGroups $settings.RestrictedGroups
|
||||||
|
if (-not $settings.SecurityPolicy) { $settings.SecurityPolicy = @{} }
|
||||||
|
$settings.SecurityPolicy['Group Membership'] = $gmEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Security Policy (GptTmpl.inf) ---
|
||||||
|
if ($settings.SecurityPolicy -and $settings.SecurityPolicy.Count -gt 0) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
Write-Host " Comparing security policy..." -ForegroundColor Yellow
|
||||||
|
$diffs = Compare-GPOSecurityPolicy -GPOName $gpoName -SecurityPolicy $settings.SecurityPolicy
|
||||||
|
|
||||||
|
if ($diffs.Count -eq 0) {
|
||||||
|
Write-Host " [OK] Security policy matches desired state" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [DRIFT] $($diffs.Count) difference(s) found:" -ForegroundColor Red
|
||||||
|
foreach ($diff in $diffs) {
|
||||||
|
Write-Host " [$($diff.Section)] $($diff.Setting): '$($diff.Current)' -> '$($diff.Desired)'" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
$totalDiffs += $diffs.Count
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " Applying security policy..." -ForegroundColor Yellow
|
||||||
|
Set-GPOSecurityPolicy -GPOName $gpoName -SecurityPolicy $settings.SecurityPolicy
|
||||||
|
$bumpMachine = $true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " No security policy settings defined." -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Registry Settings (Administrative Templates) ---
|
||||||
|
# Note: Set-GPRegistryValue handles version bumping internally — no manual bump needed
|
||||||
|
if ($settings.RegistrySettings -and $settings.RegistrySettings.Count -gt 0) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
Write-Host " Comparing registry settings..." -ForegroundColor Yellow
|
||||||
|
$regDiffs = Compare-GPORegistrySettings -GPOName $gpoName -RegistrySettings $settings.RegistrySettings
|
||||||
|
|
||||||
|
if ($regDiffs.Count -eq 0) {
|
||||||
|
Write-Host " [OK] Registry settings match desired state" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [DRIFT] $($regDiffs.Count) registry difference(s) found:" -ForegroundColor Red
|
||||||
|
foreach ($diff in $regDiffs) {
|
||||||
|
Write-Host " $($diff.Key)\$($diff.ValueName): '$($diff.Current)' -> '$($diff.Desired)'" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
$totalDiffs += $regDiffs.Count
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " Applying registry settings..." -ForegroundColor Yellow
|
||||||
|
Set-GPORegistrySettings -GPOName $gpoName -RegistrySettings $settings.RegistrySettings `
|
||||||
|
-Cleanup:(-not $NoCleanup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- GPO Link(s) ---
|
||||||
|
if ($settings.LinkTo) {
|
||||||
|
$links = Normalize-GPOLinkTo -LinkTo $settings.LinkTo
|
||||||
|
foreach ($link in $links) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
$linkDiffs = Compare-GPOLink -GPOName $gpoName -TargetOU $link.Target `
|
||||||
|
-Order $link.Order -Enforced $link.Enforced
|
||||||
|
$totalDiffs += @($linkDiffs).Count
|
||||||
|
} else {
|
||||||
|
Ensure-GPOLink -GPOName $gpoName -TargetOU $link.Target `
|
||||||
|
-Order $link.Order -Enforced $link.Enforced
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Security Filtering (Deny Apply) ---
|
||||||
|
if ($settings.SecurityFiltering -and $settings.SecurityFiltering.DenyApply) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
Write-Host " Checking security filtering..." -ForegroundColor Yellow
|
||||||
|
$filterDiffs = Compare-GPOSecurityFiltering -GPOName $gpoName -DenyApply $settings.SecurityFiltering.DenyApply
|
||||||
|
$totalDiffs += @($filterDiffs).Count
|
||||||
|
} else {
|
||||||
|
Ensure-GPOSecurityFiltering -GPOName $gpoName -DenyApply $settings.SecurityFiltering.DenyApply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- WMI Filter ---
|
||||||
|
if ($settings.WMIFilter) {
|
||||||
|
$wmiDesc = if ($settings.WMIFilter.Description) { $settings.WMIFilter.Description } else { '' }
|
||||||
|
if ($TestOnly) {
|
||||||
|
$wmiDiffs = Compare-GPOWmiFilter -GPOName $gpoName -FilterName $settings.WMIFilter.Name `
|
||||||
|
-Description $wmiDesc -Query $settings.WMIFilter.Query
|
||||||
|
$totalDiffs += @($wmiDiffs).Count
|
||||||
|
} else {
|
||||||
|
Ensure-GPOWmiFilter -GPOName $gpoName -FilterName $settings.WMIFilter.Name `
|
||||||
|
-Description $wmiDesc -Query $settings.WMIFilter.Query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Scripts (Startup / Shutdown / Logon / Logoff) ---
|
||||||
|
if ($settings.Scripts) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
$scriptDiffs = Compare-GPOScripts -GPOName $gpoName -Scripts $settings.Scripts -SourceDir $dir.FullName
|
||||||
|
$totalDiffs += @($scriptDiffs).Count
|
||||||
|
} else {
|
||||||
|
Set-GPOScripts -GPOName $gpoName -Scripts $settings.Scripts -SourceDir $dir.FullName
|
||||||
|
foreach ($type in $settings.Scripts.Keys) {
|
||||||
|
if ($type -like 'Machine*') { $bumpMachine = $true }
|
||||||
|
if ($type -like 'User*') { $bumpUser = $true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Advanced Audit Policy (audit.csv) ---
|
||||||
|
if ($settings.AdvancedAuditPolicy -and $settings.AdvancedAuditPolicy.Count -gt 0) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
$auditDiffs = Compare-GPOAdvancedAuditPolicy -GPOName $gpoName -AuditPolicy $settings.AdvancedAuditPolicy
|
||||||
|
$totalDiffs += @($auditDiffs).Count
|
||||||
|
} else {
|
||||||
|
Set-GPOAdvancedAuditPolicy -GPOName $gpoName -AuditPolicy $settings.AdvancedAuditPolicy
|
||||||
|
$bumpMachine = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Group Policy Preferences ---
|
||||||
|
if ($settings.Preferences) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
$prefDiffs = Compare-GPOPreferences -GPOName $gpoName -Preferences $settings.Preferences
|
||||||
|
$totalDiffs += @($prefDiffs).Count
|
||||||
|
} else {
|
||||||
|
Set-GPOPreferences -GPOName $gpoName -Preferences $settings.Preferences
|
||||||
|
foreach ($typeName in $settings.Preferences.Keys) {
|
||||||
|
switch ($typeName) {
|
||||||
|
'DriveMaps' { $bumpUser = $true }
|
||||||
|
'Printers' { $bumpUser = $true }
|
||||||
|
'Services' { $bumpMachine = $true }
|
||||||
|
'NetworkShares' { $bumpMachine = $true }
|
||||||
|
'LocalUsersAndGroups' { $bumpMachine = $true }
|
||||||
|
default {
|
||||||
|
foreach ($item in $settings.Preferences[$typeName]) {
|
||||||
|
if ($item.Scope -eq 'User') { $bumpUser = $true } else { $bumpMachine = $true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Firewall Profiles ---
|
||||||
|
if ($settings.FirewallProfiles) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
$fwProfileDiffs = Compare-GPOFirewallProfiles -GPOName $gpoName -FirewallProfiles $settings.FirewallProfiles
|
||||||
|
$totalDiffs += @($fwProfileDiffs).Count
|
||||||
|
} else {
|
||||||
|
Set-GPOFirewallProfiles -GPOName $gpoName -FirewallProfiles $settings.FirewallProfiles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Firewall Rules ---
|
||||||
|
if ($settings.FirewallRules -and $settings.FirewallRules.Count -gt 0) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
$fwDiffs = Compare-GPOFirewall -GPOName $gpoName -FirewallRules $settings.FirewallRules
|
||||||
|
$totalDiffs += @($fwDiffs).Count
|
||||||
|
} else {
|
||||||
|
Set-GPOFirewall -GPOName $gpoName -FirewallRules $settings.FirewallRules
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- AppLocker Policy ---
|
||||||
|
if ($settings.AppLockerPolicy) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
$alDiffs = Compare-GPOAppLockerPolicy -GPOName $gpoName -AppLockerPolicy $settings.AppLockerPolicy
|
||||||
|
$totalDiffs += @($alDiffs).Count
|
||||||
|
} else {
|
||||||
|
Set-GPOAppLockerPolicy -GPOName $gpoName -AppLockerPolicy $settings.AppLockerPolicy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- WDAC Policy ---
|
||||||
|
if ($settings.WDACPolicy) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
$wdacDiffs = Compare-GPOWdacPolicy -GPOName $gpoName -WDACPolicy $settings.WDACPolicy -SourceDir $dir.FullName
|
||||||
|
$totalDiffs += @($wdacDiffs).Count
|
||||||
|
} else {
|
||||||
|
Set-GPOWdacPolicy -GPOName $gpoName -WDACPolicy $settings.WDACPolicy -SourceDir $dir.FullName
|
||||||
|
$bumpMachine = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Folder Redirection ---
|
||||||
|
if ($settings.FolderRedirection) {
|
||||||
|
if ($TestOnly) {
|
||||||
|
$frDiffs = Compare-GPOFolderRedirection -GPOName $gpoName -FolderRedirection $settings.FolderRedirection
|
||||||
|
$totalDiffs += @($frDiffs).Count
|
||||||
|
} else {
|
||||||
|
Set-GPOFolderRedirection -GPOName $gpoName -FolderRedirection $settings.FolderRedirection
|
||||||
|
$bumpUser = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Deferred Version Bump (single bump per GPO, scope-aware) ---
|
||||||
|
if (-not $TestOnly -and ($bumpMachine -or $bumpUser)) {
|
||||||
|
$scope = if ($bumpMachine -and $bumpUser) { 'Both' }
|
||||||
|
elseif ($bumpMachine) { 'Machine' }
|
||||||
|
else { 'User' }
|
||||||
|
Update-GPOVersion -GPOName $gpoName -Scope $scope
|
||||||
|
Write-Host " GPO version bumped ($scope)." -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Summary
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
if ($TestOnly) {
|
||||||
|
if ($totalDiffs -eq 0) {
|
||||||
|
Write-Host 'All GPO settings match desired state.' -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "$totalDiffs difference(s) found." -ForegroundColor Red
|
||||||
|
}
|
||||||
|
Write-Host 'Compliance test complete. No changes were made.' -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host 'All GPO settings applied.' -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Optional: Trigger group policy refresh
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
if ($GpUpdate -and -not $TestOnly) {
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'Running gpupdate /force to process updated policies...' -ForegroundColor Cyan
|
||||||
|
$gpResult = gpupdate /force 2>&1
|
||||||
|
Write-Host ($gpResult | Out-String) -ForegroundColor Gray
|
||||||
|
Write-Host 'Group Policy refresh complete.' -ForegroundColor Green
|
||||||
|
} elseif (-not $TestOnly) {
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'TIP: Run with -GpUpdate to trigger gpupdate /force after applying.' -ForegroundColor DarkGray
|
||||||
|
Write-Host ' Or manually: gpupdate /force' -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
135
gpo/Get-UnmanagedGPOs.ps1
Normal file
135
gpo/Get-UnmanagedGPOs.ps1
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
# Get-UnmanagedGPOs.ps1
|
||||||
|
# Discovers GPOs in AD that are not managed by the framework,
|
||||||
|
# and framework-defined GPOs that do not yet exist in AD.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# .\Get-UnmanagedGPOs.ps1
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param()
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$ScriptRoot = $PSScriptRoot
|
||||||
|
|
||||||
|
. (Join-Path $ScriptRoot 'lib\GPOHelper.ps1')
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Discover managed GPO names from settings.ps1 files
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
$gpoDirs = Get-ChildItem -Path $ScriptRoot -Directory |
|
||||||
|
Where-Object { Test-Path (Join-Path $_.FullName 'settings.ps1') }
|
||||||
|
|
||||||
|
$managedNames = @{}
|
||||||
|
foreach ($dir in $gpoDirs) {
|
||||||
|
$settings = & (Join-Path $dir.FullName 'settings.ps1')
|
||||||
|
$managedNames[$settings.GPOName] = $dir.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Framework manages $($managedNames.Count) GPO(s).`n" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Enumerate all GPOs in AD
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
$allGPOs = Get-GPO -All
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Build OU link map (check known OUs for GPO links)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
$domainDN = (Get-ADDomain).DistinguishedName
|
||||||
|
$checkTargets = @(
|
||||||
|
$domainDN
|
||||||
|
"OU=Domain Controllers,$domainDN"
|
||||||
|
"OU=ExampleUsers,$domainDN"
|
||||||
|
"OU=ExampleWorkstations,$domainDN"
|
||||||
|
"OU=ExampleServers,$domainDN"
|
||||||
|
"OU=ExampleAdmins,$domainDN"
|
||||||
|
"OU=ExampleAdminWorkstations,$domainDN"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Map GPO GUID -> list of linked OU names
|
||||||
|
$linkMap = @{}
|
||||||
|
foreach ($target in $checkTargets) {
|
||||||
|
try {
|
||||||
|
$inheritance = Get-GPInheritance -Target $target -ErrorAction Stop
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract short OU name for display
|
||||||
|
$ouDisplay = if ($target -eq $domainDN) {
|
||||||
|
'(domain root)'
|
||||||
|
} else {
|
||||||
|
($target -split ',')[0] -replace '^OU=', ''
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($link in $inheritance.GpoLinks) {
|
||||||
|
$gpoId = $link.GpoId.ToString()
|
||||||
|
if (-not $linkMap.ContainsKey($gpoId)) {
|
||||||
|
$linkMap[$gpoId] = @()
|
||||||
|
}
|
||||||
|
$linkMap[$gpoId] += $ouDisplay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Section 1: Unmanaged GPOs (in AD but not in framework)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
$unmanaged = @()
|
||||||
|
foreach ($gpo in $allGPOs) {
|
||||||
|
if (-not $managedNames.ContainsKey($gpo.DisplayName)) {
|
||||||
|
$gpoId = $gpo.Id.ToString()
|
||||||
|
$linkedTo = if ($linkMap.ContainsKey($gpoId)) {
|
||||||
|
$linkMap[$gpoId] -join ', '
|
||||||
|
} else { '(not linked)' }
|
||||||
|
|
||||||
|
$unmanaged += [PSCustomObject]@{
|
||||||
|
Name = $gpo.DisplayName
|
||||||
|
Id = "{$gpoId}"
|
||||||
|
CreationTime = $gpo.CreationTime.ToString('yyyy-MM-dd')
|
||||||
|
ModificationTime = $gpo.ModificationTime.ToString('yyyy-MM-dd')
|
||||||
|
LinkedTo = $linkedTo
|
||||||
|
GpoStatus = $gpo.GpoStatus.ToString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host '=== Unmanaged GPOs in AD ===' -ForegroundColor White
|
||||||
|
if ($unmanaged.Count -eq 0) {
|
||||||
|
Write-Host 'None found. All GPOs in AD are managed by the framework.' -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "$($unmanaged.Count) GPO(s) exist in AD without a settings.ps1:`n" -ForegroundColor Yellow
|
||||||
|
$unmanaged | Format-Table Name, Id, CreationTime, ModificationTime, LinkedTo, GpoStatus -AutoSize
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Section 2: Framework GPOs not in AD (defined but not created)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
$adNames = @{}
|
||||||
|
foreach ($gpo in $allGPOs) {
|
||||||
|
$adNames[$gpo.DisplayName] = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
$notInAD = @()
|
||||||
|
foreach ($name in $managedNames.Keys) {
|
||||||
|
if (-not $adNames.ContainsKey($name)) {
|
||||||
|
$notInAD += [PSCustomObject]@{
|
||||||
|
GPOName = $name
|
||||||
|
ConfigDir = $managedNames[$name]
|
||||||
|
Status = 'Defined but not created in AD'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host '=== Framework GPOs Not in AD ===' -ForegroundColor White
|
||||||
|
if ($notInAD.Count -eq 0) {
|
||||||
|
Write-Host 'None. All framework GPOs exist in AD.' -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "$($notInAD.Count) framework GPO(s) not yet created:`n" -ForegroundColor Yellow
|
||||||
|
$notInAD | Format-Table GPOName, ConfigDir, Status -AutoSize
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Summary
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host "Summary: $($unmanaged.Count) unmanaged, $($notInAD.Count) not in AD, $($managedNames.Count) managed." -ForegroundColor Cyan
|
||||||
78
gpo/Restore-GPOBaseline.ps1
Normal file
78
gpo/Restore-GPOBaseline.ps1
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# Restore-GPOBaseline.ps1
|
||||||
|
# Interactive restore script for GPO backups.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# .\Restore-GPOBaseline.ps1 # List all backups
|
||||||
|
# .\Restore-GPOBaseline.ps1 -GPOName 'Admins-01' # List backups for one GPO
|
||||||
|
# .\Restore-GPOBaseline.ps1 -GPOName 'Admins-01' -Timestamp '20260214-153000' # Restore
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$GPOName,
|
||||||
|
[string]$Timestamp,
|
||||||
|
[switch]$Force
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$ScriptRoot = $PSScriptRoot
|
||||||
|
|
||||||
|
. (Join-Path $ScriptRoot 'lib\GPOHelper.ps1')
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# List mode (no Timestamp specified)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
if (-not $Timestamp) {
|
||||||
|
Write-Host 'Available GPO backups:' -ForegroundColor Cyan
|
||||||
|
Write-Host ''
|
||||||
|
|
||||||
|
$backups = Get-GPOBackups -GPOName $GPOName
|
||||||
|
if ($backups.Count -eq 0) {
|
||||||
|
Write-Host 'No backups found.' -ForegroundColor Yellow
|
||||||
|
if ($GPOName) {
|
||||||
|
Write-Host "Run without -GPOName to see all GPOs." -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$backups | Format-Table GPOName, Timestamp, Admin, Version -AutoSize
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'To restore, run:' -ForegroundColor DarkGray
|
||||||
|
Write-Host " .\Restore-GPOBaseline.ps1 -GPOName '<name>' -Timestamp '<timestamp>'" -ForegroundColor DarkGray
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Restore mode (Timestamp specified)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
if (-not $GPOName) {
|
||||||
|
Write-Host '-GPOName is required when specifying -Timestamp.' -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$safeName = $GPOName -replace '[^\w\-]', '_'
|
||||||
|
$backupRoot = Join-Path $ScriptRoot 'backups'
|
||||||
|
$backupPath = Join-Path $backupRoot "$safeName\$Timestamp"
|
||||||
|
|
||||||
|
if (-not (Test-Path $backupPath)) {
|
||||||
|
Write-Host "Backup not found: $backupPath" -ForegroundColor Red
|
||||||
|
Write-Host 'Run without -Timestamp to see available backups.' -ForegroundColor DarkGray
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Confirmation
|
||||||
|
if (-not $Force) {
|
||||||
|
$meta = Get-Content (Join-Path $backupPath 'metadata.json') -Raw | ConvertFrom-Json
|
||||||
|
Write-Host "About to restore GPO '$GPOName' to state from $($meta.Timestamp)" -ForegroundColor Yellow
|
||||||
|
Write-Host " Backup admin: $($meta.Admin)" -ForegroundColor Yellow
|
||||||
|
Write-Host " Backup version: $($meta.VersionNumber)" -ForegroundColor Yellow
|
||||||
|
Write-Host ''
|
||||||
|
$confirm = Read-Host 'Type YES to proceed'
|
||||||
|
if ($confirm -ne 'YES') {
|
||||||
|
Write-Host 'Aborted.' -ForegroundColor DarkGray
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Restore-GPOState -BackupPath $backupPath
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'TIP: Run Apply-GPOBaseline.ps1 -TestOnly to verify the restored state.' -ForegroundColor DarkGray
|
||||||
41
gpo/admins-01/README.md
Normal file
41
gpo/admins-01/README.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Admins-01 GPO
|
||||||
|
|
||||||
|
**GUID:** Auto-created on first `Apply-GPOBaseline.ps1` run
|
||||||
|
**Linked to:** `OU=ExampleAdmins,DC=example,DC=internal`
|
||||||
|
**Scope:** User Configuration (HKCU) -- Administrative Templates only
|
||||||
|
|
||||||
|
This GPO applies to delegated administrator accounts in the ExampleAdmins OU. Unlike Users-01, it does NOT restrict access to management tools (regedit, cmd, Run, etc.). Instead it focuses on session security and accountability.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
### Session Security
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| ScreenSaveActive | 1 | Enable screensaver (required for lock timeout) |
|
||||||
|
| ScreenSaveTimeOut | 600 | Lock screen after 10 minutes idle |
|
||||||
|
| ScreenSaverIsSecure | 1 | Require password to unlock |
|
||||||
|
|
||||||
|
### Accountability
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| EnableScriptBlockLogging | 1 | Logs all PowerShell script blocks to event log |
|
||||||
|
| EnableTranscripting | 1 | Full transcript of all PowerShell sessions |
|
||||||
|
|
||||||
|
### Taskbar Cleanup
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| TurnOffWindowsCopilot | 1 | Disables Windows Copilot |
|
||||||
|
| TaskbarDa | 0 | Hides Widgets |
|
||||||
|
| SearchboxTaskbarMode | 0 | Hides Search box |
|
||||||
|
|
||||||
|
## Design Rationale
|
||||||
|
|
||||||
|
Admins need unrestricted access to system tools. The policies here enforce:
|
||||||
|
1. **Session security** -- unattended admin sessions auto-lock after 10 minutes
|
||||||
|
2. **Audit trail** -- all PowerShell activity is logged for forensic review
|
||||||
|
3. **Clean workspace** -- distracting taskbar elements removed
|
||||||
|
|
||||||
|
Actual admin privileges come from membership in the DelegatedAdmins security group, not from this GPO.
|
||||||
98
gpo/admins-01/settings.ps1
Normal file
98
gpo/admins-01/settings.ps1
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# Admins-01 -- Settings Declaration
|
||||||
|
# Linked to: OU=ExampleAdmins,DC=example,DC=internal
|
||||||
|
#
|
||||||
|
# This GPO targets delegated administrator accounts.
|
||||||
|
# No desktop restrictions -- admins need access to management tools.
|
||||||
|
# Focus is on accountability (logging) and session security (screen lock).
|
||||||
|
# All settings are User Configuration (HKCU).
|
||||||
|
|
||||||
|
@{
|
||||||
|
GPOName = 'Admins-01'
|
||||||
|
Description = 'Admin account policy -- session lock, PS logging, taskbar cleanup'
|
||||||
|
|
||||||
|
DisableComputerConfiguration = $true
|
||||||
|
|
||||||
|
LinkTo = 'OU=ExampleAdmins,DC=example,DC=internal'
|
||||||
|
|
||||||
|
# No security policy settings -- admin privileges come from group membership, not GPO
|
||||||
|
SecurityPolicy = @{}
|
||||||
|
|
||||||
|
RegistrySettings = @(
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Session Security -- Screensaver Lock
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# Enable screensaver (required for timeout lock to work)
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Policies\Microsoft\Windows\Control Panel\Desktop'
|
||||||
|
ValueName = 'ScreenSaveActive'
|
||||||
|
Type = 'String'
|
||||||
|
Value = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Screensaver timeout: 10 minutes (600 seconds)
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Policies\Microsoft\Windows\Control Panel\Desktop'
|
||||||
|
ValueName = 'ScreenSaveTimeOut'
|
||||||
|
Type = 'String'
|
||||||
|
Value = '600'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Password-protect the screensaver (require unlock)
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Policies\Microsoft\Windows\Control Panel\Desktop'
|
||||||
|
ValueName = 'ScreenSaverIsSecure'
|
||||||
|
Type = 'String'
|
||||||
|
Value = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Accountability -- PowerShell Logging
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# Enable PowerShell script block logging
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging'
|
||||||
|
ValueName = 'EnableScriptBlockLogging'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enable PowerShell transcription
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Policies\Microsoft\Windows\PowerShell\Transcription'
|
||||||
|
ValueName = 'EnableTranscripting'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Taskbar Cleanup
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# Disable Windows Copilot
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Policies\Microsoft\Windows\WindowsCopilot'
|
||||||
|
ValueName = 'TurnOffWindowsCopilot'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hide Widgets on taskbar
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced'
|
||||||
|
ValueName = 'TaskbarDa'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hide Search box on taskbar (0=Hidden, 1=Icon, 2=Full box)
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Search'
|
||||||
|
ValueName = 'SearchboxTaskbarMode'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
45
gpo/adminworkstations-01/Install-RSAT.ps1
Normal file
45
gpo/adminworkstations-01/Install-RSAT.ps1
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Install-RSAT.ps1
|
||||||
|
# GPO Startup Script — installs RSAT Features on Demand if not already present.
|
||||||
|
# Runs as SYSTEM at computer startup. Idempotent — safe to run repeatedly.
|
||||||
|
# Requires internet access (downloads from Windows Update).
|
||||||
|
|
||||||
|
$logFile = 'C:\PSlogs\RSAT-Install.log'
|
||||||
|
|
||||||
|
# Ensure log and transcript directories exist
|
||||||
|
$logDir = Split-Path $logFile -Parent
|
||||||
|
if (-not (Test-Path $logDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
$transcriptDir = 'C:\PSlogs\Transcripts'
|
||||||
|
if (-not (Test-Path $transcriptDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $transcriptDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Log {
|
||||||
|
param([string]$Message)
|
||||||
|
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
"$ts $Message" | Out-File -Append -FilePath $logFile
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log '=== RSAT Install Check Starting ==='
|
||||||
|
|
||||||
|
$rsatFeatures = Get-WindowsCapability -Name 'Rsat*' -Online |
|
||||||
|
Where-Object { $_.State -ne 'Installed' }
|
||||||
|
|
||||||
|
if ($rsatFeatures.Count -eq 0) {
|
||||||
|
Write-Log 'All RSAT features already installed. Nothing to do.'
|
||||||
|
} else {
|
||||||
|
Write-Log "$($rsatFeatures.Count) RSAT feature(s) not installed. Installing..."
|
||||||
|
foreach ($feature in $rsatFeatures) {
|
||||||
|
Write-Log " Installing: $($feature.Name)"
|
||||||
|
try {
|
||||||
|
Add-WindowsCapability -Online -Name $feature.Name -ErrorAction Stop | Out-Null
|
||||||
|
Write-Log " [OK] $($feature.Name)"
|
||||||
|
} catch {
|
||||||
|
Write-Log " [FAILED] $($feature.Name): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Log 'RSAT installation pass complete.'
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log '=== RSAT Install Check Finished ==='
|
||||||
111
gpo/adminworkstations-01/README.md
Normal file
111
gpo/adminworkstations-01/README.md
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# AdminWorkstations-01 GPO
|
||||||
|
|
||||||
|
Privileged Access Workstation (PAW) policy for admin endpoints in the ExampleAdminWorkstations OU.
|
||||||
|
|
||||||
|
## Linked To
|
||||||
|
|
||||||
|
`OU=ExampleAdminWorkstations,DC=example,DC=internal`
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
Builds on the same foundation as Workstations-01 but with:
|
||||||
|
|
||||||
|
- **Full audit coverage** -- every category audits both success and failure (including process tracking)
|
||||||
|
- **PowerShell transcription** -- complete session recording to `C:\PSlogs\Transcripts` for forensics
|
||||||
|
- **Module logging** -- all PowerShell modules logged
|
||||||
|
- **Command line in process creation** -- Event ID 4688 includes full command line
|
||||||
|
- **Larger event logs** -- 2x workstation sizes to accommodate heavier admin activity
|
||||||
|
- **Tighter inactivity timeout** -- 10 min vs 15 min for workstations
|
||||||
|
|
||||||
|
## WMI Filter
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| Name | Workstations Only |
|
||||||
|
| Query | `SELECT * FROM Win32_OperatingSystem WHERE ProductType = 1` |
|
||||||
|
|
||||||
|
Defense-in-depth: ensures this GPO only applies to workstation operating systems.
|
||||||
|
|
||||||
|
## Restricted Groups
|
||||||
|
|
||||||
|
| Local Group | Enforced Members |
|
||||||
|
|---|---|
|
||||||
|
| BUILTIN\Administrators | Domain Admins, MasterAdmins |
|
||||||
|
|
||||||
|
Any locally-added administrator accounts are removed on next GPO refresh.
|
||||||
|
|
||||||
|
## Security Policy Settings (GptTmpl.inf)
|
||||||
|
|
||||||
|
### System Access
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| EnableGuestAccount | 0 | Local guest account disabled |
|
||||||
|
|
||||||
|
### Event Audit
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| AuditSystemEvents | 3 | Success + Failure |
|
||||||
|
| AuditLogonEvents | 3 | Success + Failure |
|
||||||
|
| AuditObjectAccess | 3 | Success + Failure |
|
||||||
|
| AuditPrivilegeUse | 3 | Success + Failure |
|
||||||
|
| AuditPolicyChange | 3 | Success + Failure |
|
||||||
|
| AuditAccountManage | 3 | Success + Failure |
|
||||||
|
| AuditProcessTracking | 1 | Success |
|
||||||
|
| AuditDSAccess | 0 | None (not a DC) |
|
||||||
|
| AuditAccountLogon | 3 | Success + Failure |
|
||||||
|
|
||||||
|
### Registry Values (Security Options)
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| InactivityTimeoutSecs | 600 | Auto-lock after 10 minutes |
|
||||||
|
| DontDisplayLastUserName | 1 | Don't show last user at login screen |
|
||||||
|
| DisableCAD | 0 | Require Ctrl+Alt+Del |
|
||||||
|
| LocalAccountTokenFilterPolicy | 1 | Allow unfiltered admin tokens over WinRM (enables remote GPO/AD management without RDP) |
|
||||||
|
|
||||||
|
## Registry Settings (Administrative Templates)
|
||||||
|
|
||||||
|
### Autorun / Autoplay
|
||||||
|
|
||||||
|
| Key | ValueName | Value | Effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Policies\Explorer | NoDriveTypeAutoRun | 255 | Disable autorun on all drives |
|
||||||
|
| Policies\Explorer | NoAutorun | 1 | Disable autoplay |
|
||||||
|
|
||||||
|
### Windows Update
|
||||||
|
|
||||||
|
| Key | ValueName | Value | Effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| WindowsUpdate\AU | NoAutoUpdate | 0 | Enable automatic updates |
|
||||||
|
| WindowsUpdate\AU | AUOptions | 4 | Auto download + schedule install |
|
||||||
|
| WindowsUpdate\AU | ScheduledInstallDay | 0 | Every day |
|
||||||
|
| WindowsUpdate\AU | ScheduledInstallTime | 3 | 3:00 AM |
|
||||||
|
|
||||||
|
### Logging & Auditing
|
||||||
|
|
||||||
|
| Key | ValueName | Value | Effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| PowerShell\ScriptBlockLogging | EnableScriptBlockLogging | 1 | Log all script blocks |
|
||||||
|
| PowerShell\Transcription | EnableTranscripting | 1 | Record full PS sessions |
|
||||||
|
| PowerShell\Transcription | OutputDirectory | C:\PSlogs\Transcripts | Transcript save location |
|
||||||
|
| PowerShell\Transcription | EnableInvocationHeader | 1 | Timestamp per command |
|
||||||
|
| PowerShell\ModuleLogging | EnableModuleLogging | 1 | Log all module activity |
|
||||||
|
| PowerShell\ModuleLogging\ModuleNames | * | * | All modules |
|
||||||
|
| System\Audit | ProcessCreationIncludeCmdLine_Enabled | 1 | Command line in Event 4688 |
|
||||||
|
|
||||||
|
### Event Log Sizes
|
||||||
|
|
||||||
|
| Log | Size | vs. Workstations-01 |
|
||||||
|
|---|---|---|
|
||||||
|
| Application | 64 MB | 2x |
|
||||||
|
| Security | 256 MB | ~1.3x |
|
||||||
|
| System | 64 MB | 2x |
|
||||||
|
| PowerShell | 64 MB | new |
|
||||||
|
|
||||||
|
### Remote Desktop
|
||||||
|
|
||||||
|
| Key | ValueName | Value | Effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Terminal Services | UserAuthentication | 1 | Require NLA |
|
||||||
169
gpo/adminworkstations-01/WDACPolicy.xml
Normal file
169
gpo/adminworkstations-01/WDACPolicy.xml
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
WDAC Baseline: AllowMicrosoft (Audit Mode)
|
||||||
|
|
||||||
|
Trusts all binaries signed by Microsoft root certificates and WHQL-certified
|
||||||
|
drivers. Audit mode logs CodeIntegrity event 3076 for anything that would be
|
||||||
|
blocked, without actually blocking execution.
|
||||||
|
|
||||||
|
Review audit logs before switching to enforce mode:
|
||||||
|
Get-WinEvent -LogName 'Microsoft-Windows-CodeIntegrity/Operational' |
|
||||||
|
Where-Object Id -eq 3076
|
||||||
|
|
||||||
|
Based on Microsoft's AllowMicrosoft template.
|
||||||
|
-->
|
||||||
|
<SiPolicy xmlns="urn:schemas-microsoft-com:sipolicy" PolicyType="Base Policy">
|
||||||
|
<VersionEx>10.0.1.0</VersionEx>
|
||||||
|
<PolicyID>{7BE95702-9AD4-402C-BCCE-87D8587E0F7D}</PolicyID>
|
||||||
|
<BasePolicyID>{7BE95702-9AD4-402C-BCCE-87D8587E0F7D}</BasePolicyID>
|
||||||
|
<PlatformID>{2E07F7E4-194C-4D20-B7C9-6F44A6C5A234}</PlatformID>
|
||||||
|
|
||||||
|
<Rules>
|
||||||
|
<!-- User-mode code integrity: enforce policy on applications, not just drivers -->
|
||||||
|
<Rule><Option>Enabled:UMCI</Option></Rule>
|
||||||
|
<!-- AUDIT MODE: log violations (event 3076) without blocking -->
|
||||||
|
<Rule><Option>Enabled:Audit Mode</Option></Rule>
|
||||||
|
<!-- Do not trust Windows Insider / flighting-signed binaries -->
|
||||||
|
<Rule><Option>Disabled:Flight Signing</Option></Rule>
|
||||||
|
<!-- Policy is unsigned (no secure boot signing required) -->
|
||||||
|
<Rule><Option>Enabled:Unsigned System Integrity Policy</Option></Rule>
|
||||||
|
<!-- Allow F8 boot menu for physically present users -->
|
||||||
|
<Rule><Option>Enabled:Advanced Boot Options Menu</Option></Rule>
|
||||||
|
<!-- Policy updates apply without reboot -->
|
||||||
|
<Rule><Option>Enabled:Update Policy No Reboot</Option></Rule>
|
||||||
|
<!-- Allow supplemental policies to extend this base -->
|
||||||
|
<Rule><Option>Enabled:Allow Supplemental Policies</Option></Rule>
|
||||||
|
</Rules>
|
||||||
|
|
||||||
|
<EKUs>
|
||||||
|
<EKU ID="ID_EKU_WINDOWS" FriendlyName="Windows System Component Verification - 1.3.6.1.4.1.311.10.3.6" Value="010A2B0601040182370A0306" />
|
||||||
|
<EKU ID="ID_EKU_WHQL" FriendlyName="WHQL Crypto - 1.3.6.1.4.1.311.10.3.5" Value="010A2B0601040182370A0305" />
|
||||||
|
<EKU ID="ID_EKU_ELAM" FriendlyName="Early Launch AntiMalware - 1.3.6.1.4.1.311.61.4.1" Value="010A2B0601040182373D0401" />
|
||||||
|
<EKU ID="ID_EKU_HAL_EXT" FriendlyName="HAL Extension - 1.3.6.1.4.1.311.61.5.1" Value="010A2B0601040182373D0501" />
|
||||||
|
<EKU ID="ID_EKU_STORE" FriendlyName="Windows Store - 1.3.6.1.4.1.311.76.3.1" Value="010A2B0601040182374C0301" />
|
||||||
|
</EKUs>
|
||||||
|
|
||||||
|
<FileRules>
|
||||||
|
<!-- RefreshPolicy.exe: allows policy refresh without reboot -->
|
||||||
|
<FileAttrib ID="ID_FILEATTRIB_REFRESH_POLICY" FriendlyName="RefreshPolicy.exe FileAttribute" FileName="RefreshPolicy.exe" MinimumFileVersion="10.0.19042.0" />
|
||||||
|
</FileRules>
|
||||||
|
|
||||||
|
<Signers>
|
||||||
|
<!-- ============================================================= -->
|
||||||
|
<!-- Kernel-mode signers -->
|
||||||
|
<!-- ============================================================= -->
|
||||||
|
|
||||||
|
<!-- Microsoft Product Root 2010: all Microsoft-signed binaries -->
|
||||||
|
<Signer ID="ID_SIGNER_MICROSOFT_PRODUCTION" Name="Microsoft Product Root 2010">
|
||||||
|
<CertRoot Type="Wellknown" Value="06" />
|
||||||
|
</Signer>
|
||||||
|
|
||||||
|
<!-- Microsoft Product Root 2001: legacy Microsoft-signed binaries -->
|
||||||
|
<Signer ID="ID_SIGNER_MICROSOFT_2001" Name="Microsoft Product Root 2001">
|
||||||
|
<CertRoot Type="Wellknown" Value="05" />
|
||||||
|
</Signer>
|
||||||
|
|
||||||
|
<!-- Microsoft Product Root 1997: oldest legacy binaries -->
|
||||||
|
<Signer ID="ID_SIGNER_MICROSOFT_1997" Name="Microsoft Product Root 1997">
|
||||||
|
<CertRoot Type="Wellknown" Value="04" />
|
||||||
|
</Signer>
|
||||||
|
|
||||||
|
<!-- Microsoft Standard Root 2011: standard-signed applications -->
|
||||||
|
<Signer ID="ID_SIGNER_MICROSOFT_STANDARD" Name="Microsoft Standard Root 2011">
|
||||||
|
<CertRoot Type="Wellknown" Value="07" />
|
||||||
|
</Signer>
|
||||||
|
|
||||||
|
<!-- Microsoft Code Verification Root 2006 -->
|
||||||
|
<Signer ID="ID_SIGNER_MICROSOFT_CODEVERIF" Name="Microsoft Code Verification Root 2006">
|
||||||
|
<CertRoot Type="Wellknown" Value="08" />
|
||||||
|
</Signer>
|
||||||
|
|
||||||
|
<!-- ============================================================= -->
|
||||||
|
<!-- User-mode signer duplicates (same roots, separate IDs) -->
|
||||||
|
<!-- ============================================================= -->
|
||||||
|
|
||||||
|
<Signer ID="ID_SIGNER_MICROSOFT_PRODUCTION_USER" Name="Microsoft Product Root 2010">
|
||||||
|
<CertRoot Type="Wellknown" Value="06" />
|
||||||
|
</Signer>
|
||||||
|
|
||||||
|
<Signer ID="ID_SIGNER_MICROSOFT_2001_USER" Name="Microsoft Product Root 2001">
|
||||||
|
<CertRoot Type="Wellknown" Value="05" />
|
||||||
|
</Signer>
|
||||||
|
|
||||||
|
<Signer ID="ID_SIGNER_MICROSOFT_1997_USER" Name="Microsoft Product Root 1997">
|
||||||
|
<CertRoot Type="Wellknown" Value="04" />
|
||||||
|
</Signer>
|
||||||
|
|
||||||
|
<Signer ID="ID_SIGNER_MICROSOFT_STANDARD_USER" Name="Microsoft Standard Root 2011">
|
||||||
|
<CertRoot Type="Wellknown" Value="07" />
|
||||||
|
</Signer>
|
||||||
|
|
||||||
|
<Signer ID="ID_SIGNER_MICROSOFT_CODEVERIF_USER" Name="Microsoft Code Verification Root 2006">
|
||||||
|
<CertRoot Type="Wellknown" Value="08" />
|
||||||
|
</Signer>
|
||||||
|
|
||||||
|
<!-- ============================================================= -->
|
||||||
|
<!-- User-mode-only signers -->
|
||||||
|
<!-- ============================================================= -->
|
||||||
|
|
||||||
|
<!-- Windows Store (MarketPlace PCA 2011) -->
|
||||||
|
<Signer ID="ID_SIGNER_STORE" Name="Microsoft MarketPlace PCA 2011">
|
||||||
|
<CertRoot Type="TBS" Value="FC9EDE3DCCA09186B2D3BF9B738A2050CB1A554DA2DCADB55F3F72EE17721378" />
|
||||||
|
<CertEKU ID="ID_EKU_STORE" />
|
||||||
|
</Signer>
|
||||||
|
|
||||||
|
<!-- RefreshPolicy.exe signer (Code Signing PCA 2011) -->
|
||||||
|
<Signer ID="ID_SIGNER_MICROSOFT_REFRESH_POLICY" Name="Microsoft Code Signing PCA 2011">
|
||||||
|
<CertRoot Type="TBS" Value="F6F717A43AD9ABDDC8CEFDDE1C505462535E7D1307E630F9544A2D14FE8BF26E" />
|
||||||
|
<CertPublisher Value="Microsoft Corporation" />
|
||||||
|
<FileAttribRef RuleID="ID_FILEATTRIB_REFRESH_POLICY" />
|
||||||
|
</Signer>
|
||||||
|
</Signers>
|
||||||
|
|
||||||
|
<SigningScenarios>
|
||||||
|
<!-- Kernel Mode (131): drivers and kernel components -->
|
||||||
|
<SigningScenario Value="131" ID="ID_SIGNINGSCENARIO_KMCI" FriendlyName="Kernel Mode Signing Scenario">
|
||||||
|
<ProductSigners>
|
||||||
|
<AllowedSigners>
|
||||||
|
<AllowedSigner SignerId="ID_SIGNER_MICROSOFT_PRODUCTION" />
|
||||||
|
<AllowedSigner SignerId="ID_SIGNER_MICROSOFT_2001" />
|
||||||
|
<AllowedSigner SignerId="ID_SIGNER_MICROSOFT_1997" />
|
||||||
|
<AllowedSigner SignerId="ID_SIGNER_MICROSOFT_STANDARD" />
|
||||||
|
<AllowedSigner SignerId="ID_SIGNER_MICROSOFT_CODEVERIF" />
|
||||||
|
</AllowedSigners>
|
||||||
|
</ProductSigners>
|
||||||
|
</SigningScenario>
|
||||||
|
|
||||||
|
<!-- User Mode (12): applications, DLLs, scripts -->
|
||||||
|
<SigningScenario Value="12" ID="ID_SIGNINGSCENARIO_UMCI" FriendlyName="User Mode Signing Scenario">
|
||||||
|
<ProductSigners>
|
||||||
|
<AllowedSigners>
|
||||||
|
<AllowedSigner SignerId="ID_SIGNER_MICROSOFT_PRODUCTION_USER" />
|
||||||
|
<AllowedSigner SignerId="ID_SIGNER_MICROSOFT_2001_USER" />
|
||||||
|
<AllowedSigner SignerId="ID_SIGNER_MICROSOFT_1997_USER" />
|
||||||
|
<AllowedSigner SignerId="ID_SIGNER_MICROSOFT_STANDARD_USER" />
|
||||||
|
<AllowedSigner SignerId="ID_SIGNER_MICROSOFT_CODEVERIF_USER" />
|
||||||
|
<AllowedSigner SignerId="ID_SIGNER_STORE" />
|
||||||
|
<AllowedSigner SignerId="ID_SIGNER_MICROSOFT_REFRESH_POLICY" />
|
||||||
|
</AllowedSigners>
|
||||||
|
</ProductSigners>
|
||||||
|
</SigningScenario>
|
||||||
|
</SigningScenarios>
|
||||||
|
|
||||||
|
<UpdatePolicySigners />
|
||||||
|
|
||||||
|
<CiSigners>
|
||||||
|
<CiSigner SignerId="ID_SIGNER_STORE" />
|
||||||
|
</CiSigners>
|
||||||
|
|
||||||
|
<HvciOptions>0</HvciOptions>
|
||||||
|
|
||||||
|
<Settings>
|
||||||
|
<Setting Provider="PolicyInfo" Key="Information" ValueName="Name">
|
||||||
|
<Value><String>Example-WDAC-AllowMicrosoft-Audit</String></Value>
|
||||||
|
</Setting>
|
||||||
|
<Setting Provider="PolicyInfo" Key="Information" ValueName="Id">
|
||||||
|
<Value><String>7BE95702</String></Value>
|
||||||
|
</Setting>
|
||||||
|
</Settings>
|
||||||
|
</SiPolicy>
|
||||||
470
gpo/adminworkstations-01/settings.ps1
Normal file
470
gpo/adminworkstations-01/settings.ps1
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
# AdminWorkstations-01 -- Settings Declaration
|
||||||
|
# Linked to: OU=ExampleAdminWorkstations,DC=example,DC=internal
|
||||||
|
#
|
||||||
|
# Privileged Access Workstation (PAW) policy. Inherits the spirit of Workstations-01
|
||||||
|
# but with stricter logging, tighter lockout, and enhanced forensic capability.
|
||||||
|
# All settings are Computer Configuration (HKLM).
|
||||||
|
|
||||||
|
@{
|
||||||
|
GPOName = 'AdminWorkstations-01'
|
||||||
|
Description = 'Privileged Access Workstation (PAW) -- enhanced logging, strict lockout, forensic capability'
|
||||||
|
|
||||||
|
DisableUserConfiguration = $true
|
||||||
|
|
||||||
|
LinkTo = 'OU=ExampleAdminWorkstations,DC=example,DC=internal'
|
||||||
|
|
||||||
|
# Defense-in-depth: only apply to workstation OS (ProductType 1).
|
||||||
|
# Prevents misapplication if a server object lands in the wrong OU.
|
||||||
|
WMIFilter = @{
|
||||||
|
Name = 'Workstations Only'
|
||||||
|
Description = 'Targets workstation operating systems (ProductType = 1)'
|
||||||
|
Query = "SELECT * FROM Win32_OperatingSystem WHERE ProductType = 1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lock down local Administrators to Domain Admins + MasterAdmins only.
|
||||||
|
# Any unauthorized additions are removed on next GPO refresh.
|
||||||
|
RestrictedGroups = @{
|
||||||
|
'BUILTIN\Administrators' = @{
|
||||||
|
Members = @('EXAMPLE\Domain Admins', 'EXAMPLE\MasterAdmins')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SecurityPolicy = @{
|
||||||
|
|
||||||
|
'System Access' = [ordered]@{
|
||||||
|
EnableGuestAccount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
'Event Audit' = [ordered]@{
|
||||||
|
# Everything audited -- admin machines need full visibility
|
||||||
|
AuditSystemEvents = 3 # Success + Failure
|
||||||
|
AuditLogonEvents = 3 # Success + Failure
|
||||||
|
AuditObjectAccess = 3 # Success + Failure
|
||||||
|
AuditPrivilegeUse = 3 # Success + Failure
|
||||||
|
AuditPolicyChange = 3 # Success + Failure
|
||||||
|
AuditAccountManage = 3 # Success + Failure
|
||||||
|
AuditProcessTracking = 1 # Success (track what admins run)
|
||||||
|
AuditDSAccess = 0 # No auditing (not a DC)
|
||||||
|
AuditAccountLogon = 3 # Success + Failure
|
||||||
|
}
|
||||||
|
|
||||||
|
'Registry Values' = [ordered]@{
|
||||||
|
# Interactive logon: Machine inactivity limit -- 10 min (stricter than workstations)
|
||||||
|
'MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\InactivityTimeoutSecs' = '4,600'
|
||||||
|
|
||||||
|
# Interactive logon: Don't display last signed-in user
|
||||||
|
'MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\DontDisplayLastUserName' = '4,1'
|
||||||
|
|
||||||
|
# Interactive logon: Require CTRL+ALT+DEL
|
||||||
|
'MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\DisableCAD' = '4,0'
|
||||||
|
|
||||||
|
# Allow admin accounts to receive unfiltered tokens over WinRM (e.g. Invoke-Command)
|
||||||
|
# Required for remote GPO/AD baseline management without RDP to DC
|
||||||
|
'MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\LocalAccountTokenFilterPolicy' = '4,1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Windows Firewall -- default-deny inbound, allow management traffic
|
||||||
|
# PAWs need inbound WinRM so the DC can manage them remotely.
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
FirewallProfiles = @{
|
||||||
|
Domain = @{ Enabled = $true; DefaultInboundAction = 'Block'; DefaultOutboundAction = 'Allow' }
|
||||||
|
Private = @{ Enabled = $true; DefaultInboundAction = 'Block'; DefaultOutboundAction = 'Allow' }
|
||||||
|
Public = @{ Enabled = $true; DefaultInboundAction = 'Block'; DefaultOutboundAction = 'Allow' }
|
||||||
|
}
|
||||||
|
|
||||||
|
FirewallRules = @(
|
||||||
|
# WinRM -- remote management
|
||||||
|
@{
|
||||||
|
DisplayName = 'Allow WinRM HTTP (Inbound)'
|
||||||
|
Direction = 'Inbound'
|
||||||
|
Action = 'Allow'
|
||||||
|
Protocol = 'TCP'
|
||||||
|
LocalPort = '5985'
|
||||||
|
Profile = 'Domain'
|
||||||
|
Description = 'Windows Remote Management (HTTP) for domain management'
|
||||||
|
}
|
||||||
|
|
||||||
|
# RDP -- remote assistance
|
||||||
|
@{
|
||||||
|
DisplayName = 'Allow RDP (Inbound)'
|
||||||
|
Direction = 'Inbound'
|
||||||
|
Action = 'Allow'
|
||||||
|
Protocol = 'TCP'
|
||||||
|
LocalPort = '3389'
|
||||||
|
Profile = 'Domain'
|
||||||
|
Description = 'Remote Desktop Protocol for domain-authenticated sessions'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ICMP Echo -- network troubleshooting
|
||||||
|
@{
|
||||||
|
DisplayName = 'Allow ICMP Echo Request (Inbound)'
|
||||||
|
Direction = 'Inbound'
|
||||||
|
Action = 'Allow'
|
||||||
|
Protocol = 'ICMPv4'
|
||||||
|
Profile = 'Domain'
|
||||||
|
Description = 'ICMP echo for ping-based health monitoring'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Advanced Audit Policy -- granular subcategory-level auditing
|
||||||
|
# Overrides the legacy Event Audit above with 53 subcategories.
|
||||||
|
# PAWs need full visibility into admin actions.
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
AdvancedAuditPolicy = @{
|
||||||
|
# System
|
||||||
|
'Security State Change' = 'Success and Failure'
|
||||||
|
'Security System Extension' = 'Success and Failure'
|
||||||
|
'System Integrity' = 'Success and Failure'
|
||||||
|
|
||||||
|
# Logon/Logoff
|
||||||
|
'Logon' = 'Success and Failure'
|
||||||
|
'Logoff' = 'Success'
|
||||||
|
'Account Lockout' = 'Success'
|
||||||
|
'Special Logon' = 'Success and Failure'
|
||||||
|
'Other Logon/Logoff Events' = 'Success and Failure'
|
||||||
|
'Group Membership' = 'Success'
|
||||||
|
|
||||||
|
# Object Access
|
||||||
|
'File System' = 'Success and Failure'
|
||||||
|
'Registry' = 'Success and Failure'
|
||||||
|
'SAM' = 'Success and Failure'
|
||||||
|
'Removable Storage' = 'Success and Failure'
|
||||||
|
|
||||||
|
# Privilege Use
|
||||||
|
'Sensitive Privilege Use' = 'Success and Failure'
|
||||||
|
|
||||||
|
# Detailed Tracking -- track what admins run
|
||||||
|
'Process Creation' = 'Success and Failure'
|
||||||
|
'Process Termination' = 'Success'
|
||||||
|
'DPAPI Activity' = 'Success and Failure'
|
||||||
|
'Plug and Play Events' = 'Success'
|
||||||
|
|
||||||
|
# Policy Change
|
||||||
|
'Audit Policy Change' = 'Success and Failure'
|
||||||
|
'Authentication Policy Change' = 'Success'
|
||||||
|
'MPSSVC Rule-Level Policy Change' = 'Success and Failure'
|
||||||
|
|
||||||
|
# Account Management
|
||||||
|
'User Account Management' = 'Success and Failure'
|
||||||
|
'Security Group Management' = 'Success and Failure'
|
||||||
|
|
||||||
|
# Account Logon
|
||||||
|
'Credential Validation' = 'Success and Failure'
|
||||||
|
'Kerberos Authentication Service' = 'Success and Failure'
|
||||||
|
'Kerberos Service Ticket Operations' = 'Success and Failure'
|
||||||
|
}
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# WDAC -- AllowMicrosoft baseline in audit mode
|
||||||
|
# Trusts all Microsoft-signed binaries and WHQL drivers.
|
||||||
|
# Logs CodeIntegrity event 3076 for anything that would be blocked.
|
||||||
|
# Review: Get-WinEvent 'Microsoft-Windows-CodeIntegrity/Operational' | ? Id -eq 3076
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
WDACPolicy = @{
|
||||||
|
PolicyFile = 'WDACPolicy.xml'
|
||||||
|
}
|
||||||
|
|
||||||
|
Scripts = @{
|
||||||
|
MachineStartup = @(
|
||||||
|
@{
|
||||||
|
Source = 'Install-RSAT.ps1'
|
||||||
|
Parameters = ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# AppLocker -- audit mode (logs violations, does not block)
|
||||||
|
# Same baseline as Workstations-01 but deployed separately so PAWs
|
||||||
|
# can be tightened to Enabled independently once audit logs are clean.
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
AppLockerPolicy = @{
|
||||||
|
Exe = @{
|
||||||
|
EnforcementMode = 'AuditOnly'
|
||||||
|
Rules = @(
|
||||||
|
@{
|
||||||
|
Type = 'Publisher'
|
||||||
|
Name = 'Allow Microsoft-signed executables'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'Everyone'
|
||||||
|
Publisher = 'O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US'
|
||||||
|
Product = '*'
|
||||||
|
Binary = '*'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Type = 'Path'
|
||||||
|
Name = 'Allow executables in Program Files'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'Everyone'
|
||||||
|
Path = '%PROGRAMFILES%\*'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Type = 'Path'
|
||||||
|
Name = 'Allow executables in Windows directory'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'Everyone'
|
||||||
|
Path = '%WINDIR%\*'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Type = 'Path'
|
||||||
|
Name = 'Allow administrators unrestricted'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'BUILTIN\Administrators'
|
||||||
|
Path = '*'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Msi = @{
|
||||||
|
EnforcementMode = 'AuditOnly'
|
||||||
|
Rules = @(
|
||||||
|
@{
|
||||||
|
Type = 'Publisher'
|
||||||
|
Name = 'Allow Microsoft-signed installers'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'Everyone'
|
||||||
|
Publisher = 'O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US'
|
||||||
|
Product = '*'
|
||||||
|
Binary = '*'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Type = 'Path'
|
||||||
|
Name = 'Allow administrators unrestricted'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'BUILTIN\Administrators'
|
||||||
|
Path = '*'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Script = @{
|
||||||
|
EnforcementMode = 'AuditOnly'
|
||||||
|
Rules = @(
|
||||||
|
@{
|
||||||
|
Type = 'Path'
|
||||||
|
Name = 'Allow scripts in Windows directory'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'Everyone'
|
||||||
|
Path = '%WINDIR%\*'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Type = 'Path'
|
||||||
|
Name = 'Allow scripts in Program Files'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'Everyone'
|
||||||
|
Path = '%PROGRAMFILES%\*'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Type = 'Path'
|
||||||
|
Name = 'Allow administrators unrestricted'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'BUILTIN\Administrators'
|
||||||
|
Path = '*'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RegistrySettings = @(
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Autorun / Autoplay
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer'
|
||||||
|
ValueName = 'NoDriveTypeAutoRun'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 255
|
||||||
|
}
|
||||||
|
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer'
|
||||||
|
ValueName = 'NoAutorun'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Windows Update
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU'
|
||||||
|
ValueName = 'NoAutoUpdate'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU'
|
||||||
|
ValueName = 'AUOptions'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU'
|
||||||
|
ValueName = 'ScheduledInstallDay'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU'
|
||||||
|
ValueName = 'ScheduledInstallTime'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Logging & Auditing -- Enhanced for admin machines
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# PowerShell script block logging
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging'
|
||||||
|
ValueName = 'EnableScriptBlockLogging'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# PowerShell transcription -- full session recording for all users on this machine
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\Transcription'
|
||||||
|
ValueName = 'EnableTranscripting'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Transcription output directory
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\Transcription'
|
||||||
|
ValueName = 'OutputDirectory'
|
||||||
|
Type = 'String'
|
||||||
|
Value = 'C:\PSlogs\Transcripts'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Include invocation headers in transcripts (timestamps per command)
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\Transcription'
|
||||||
|
ValueName = 'EnableInvocationHeader'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# PowerShell module logging -- log all module activity
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging'
|
||||||
|
ValueName = 'EnableModuleLogging'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Log all modules (wildcard)
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging\ModuleNames'
|
||||||
|
ValueName = '*'
|
||||||
|
Type = 'String'
|
||||||
|
Value = '*'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Command line in process creation events (Event ID 4688)
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\Audit'
|
||||||
|
ValueName = 'ProcessCreationIncludeCmdLine_Enabled'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Event log sizes -- larger than workstations (admin activity generates more events)
|
||||||
|
|
||||||
|
# Application log: 64 MB
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Application'
|
||||||
|
ValueName = 'MaxSize'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 65536
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security log: 256 MB
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Security'
|
||||||
|
ValueName = 'MaxSize'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 262144
|
||||||
|
}
|
||||||
|
|
||||||
|
# System log: 64 MB
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\EventLog\System'
|
||||||
|
ValueName = 'MaxSize'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 65536
|
||||||
|
}
|
||||||
|
|
||||||
|
# PowerShell Operational log: 64 MB
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Windows PowerShell'
|
||||||
|
ValueName = 'MaxSize'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 65536
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Remote Desktop
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# Require NLA for RDP
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows NT\Terminal Services'
|
||||||
|
ValueName = 'UserAuthentication'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Windows Defender Exclusions -- IDE Performance
|
||||||
|
# =============================================================
|
||||||
|
# Real-time scanning of build artifacts and caches degrades IDE
|
||||||
|
# build and startup performance. These path exclusions prevent
|
||||||
|
# Defender from scanning frequently-written IDE directories.
|
||||||
|
|
||||||
|
# Enable the path exclusions policy
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows Defender\Exclusions'
|
||||||
|
ValueName = 'Exclusions_Paths'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# JetBrains IDE settings and configuration
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows Defender\Exclusions\Paths'
|
||||||
|
ValueName = '%APPDATA%\JetBrains'
|
||||||
|
Type = 'String'
|
||||||
|
Value = '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
# JetBrains IDE caches, indexes, and Toolbox binaries
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows Defender\Exclusions\Paths'
|
||||||
|
ValueName = '%LOCALAPPDATA%\JetBrains'
|
||||||
|
Type = 'String'
|
||||||
|
Value = '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Gradle cache directory
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows Defender\Exclusions\Paths'
|
||||||
|
ValueName = '%USERPROFILE%\.gradle'
|
||||||
|
Type = 'String'
|
||||||
|
Value = '0'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
141
gpo/default-domain-controller/DefaultDCPolicy.ps1
Normal file
141
gpo/default-domain-controller/DefaultDCPolicy.ps1
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
# DefaultDCPolicy.ps1
|
||||||
|
# DSC Configuration: Default Domain Controllers Policy for example.internal
|
||||||
|
#
|
||||||
|
# SINGLE SOURCE OF TRUTH: All values are read from settings.ps1.
|
||||||
|
# Do NOT hardcode policy values here -- edit settings.ps1 instead.
|
||||||
|
#
|
||||||
|
# These settings define who can perform privileged operations on DCs
|
||||||
|
# and how DC network communications are secured.
|
||||||
|
|
||||||
|
# Load helpers for SID resolution and registry value parsing
|
||||||
|
. (Join-Path $PSScriptRoot '..\lib\GPOHelper.ps1')
|
||||||
|
|
||||||
|
# Load the authoritative settings
|
||||||
|
$dcSettings = & (Join-Path $PSScriptRoot 'settings.ps1')
|
||||||
|
$privRights = $dcSettings.SecurityPolicy['Privilege Rights']
|
||||||
|
$regValues = $dcSettings.SecurityPolicy['Registry Values']
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Privilege Rights: Se* constant -> DSC policy name mapping
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
$privilegeMap = @{
|
||||||
|
'SeAssignPrimaryTokenPrivilege' = 'Replace_a_process_level_token'
|
||||||
|
'SeAuditPrivilege' = 'Generate_security_audits'
|
||||||
|
'SeDebugPrivilege' = 'Debug_programs'
|
||||||
|
'SeBackupPrivilege' = 'Back_up_files_and_directories'
|
||||||
|
'SeRestorePrivilege' = 'Restore_files_and_directories'
|
||||||
|
'SeBatchLogonRight' = 'Log_on_as_a_batch_job'
|
||||||
|
'SeInteractiveLogonRight' = 'Allow_log_on_locally'
|
||||||
|
'SeRemoteInteractiveLogonRight' = 'Allow_log_on_through_Remote_Desktop_Services'
|
||||||
|
'SeNetworkLogonRight' = 'Access_this_computer_from_the_network'
|
||||||
|
'SeChangeNotifyPrivilege' = 'Bypass_traverse_checking'
|
||||||
|
'SeMachineAccountPrivilege' = 'Add_workstations_to_domain'
|
||||||
|
'SeEnableDelegationPrivilege' = 'Enable_computer_and_user_accounts_to_be_trusted_for_delegation'
|
||||||
|
'SeCreatePagefilePrivilege' = 'Create_a_pagefile'
|
||||||
|
'SeIncreaseBasePriorityPrivilege' = 'Increase_scheduling_priority'
|
||||||
|
'SeIncreaseQuotaPrivilege' = 'Adjust_memory_quotas_for_a_process'
|
||||||
|
'SeLoadDriverPrivilege' = 'Load_and_unload_device_drivers'
|
||||||
|
'SeProfileSingleProcessPrivilege' = 'Profile_single_process'
|
||||||
|
'SeSystemProfilePrivilege' = 'Profile_system_performance'
|
||||||
|
'SeRemoteShutdownPrivilege' = 'Force_shutdown_from_a_remote_system'
|
||||||
|
'SeShutdownPrivilege' = 'Shut_down_the_system'
|
||||||
|
'SeSecurityPrivilege' = 'Manage_auditing_and_security_log'
|
||||||
|
'SeTakeOwnershipPrivilege' = 'Take_ownership_of_files_or_other_objects'
|
||||||
|
'SeSystemEnvironmentPrivilege' = 'Modify_firmware_environment_values'
|
||||||
|
'SeSystemTimePrivilege' = 'Change_the_system_time'
|
||||||
|
'SeUndockPrivilege' = 'Remove_computer_from_docking_station'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build DSC-friendly privilege list: resolve SIDs to NTAccount names
|
||||||
|
$dscPrivileges = @()
|
||||||
|
foreach ($seConst in $privRights.Keys) {
|
||||||
|
if (-not $privilegeMap.ContainsKey($seConst)) {
|
||||||
|
Write-Warning "Unknown privilege constant '$seConst' -- no DSC mapping. Skipping."
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$dscPrivileges += @{
|
||||||
|
ResourceName = $seConst -replace '^Se', ''
|
||||||
|
Policy = $privilegeMap[$seConst]
|
||||||
|
Identity = Resolve-SIDsToNames $privRights[$seConst]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Registry Values -> SecurityOption mapping (semantic)
|
||||||
|
# The mapping from DWORD values to DSC enum strings is
|
||||||
|
# setting-specific and must be maintained here.
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
$dcRegToSecOption = @{
|
||||||
|
'LDAPServerIntegrity' = @{
|
||||||
|
DscProperty = 'Domain_controller_LDAP_server_signing_requirements'
|
||||||
|
ValueMap = @{ 1 = 'None'; 2 = 'Require signing' }
|
||||||
|
}
|
||||||
|
'RequireSignOrSeal' = @{
|
||||||
|
DscProperty = 'Domain_member_Digitally_encrypt_or_sign_secure_channel_data_always'
|
||||||
|
ValueMap = @{ 0 = 'Disabled'; 1 = 'Enabled' }
|
||||||
|
}
|
||||||
|
'RequireSecuritySignature' = @{
|
||||||
|
DscProperty = 'Microsoft_network_server_Digitally_sign_communications_always'
|
||||||
|
ValueMap = @{ 0 = 'Disabled'; 1 = 'Enabled' }
|
||||||
|
}
|
||||||
|
'EnableSecuritySignature' = @{
|
||||||
|
DscProperty = 'Microsoft_network_server_Digitally_sign_communications_if_client_agrees'
|
||||||
|
ValueMap = @{ 0 = 'Disabled'; 1 = 'Enabled' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract short key name from full registry paths and map to DSC values
|
||||||
|
$dscSecOptions = @{}
|
||||||
|
foreach ($regPath in $regValues.Keys) {
|
||||||
|
$shortKey = ($regPath -split '\\')[-1]
|
||||||
|
|
||||||
|
if ($dcRegToSecOption.ContainsKey($shortKey)) {
|
||||||
|
$mapping = $dcRegToSecOption[$shortKey]
|
||||||
|
$numValue = Get-GptTmplRegValue $regValues[$regPath]
|
||||||
|
$dscValue = $mapping.ValueMap[$numValue]
|
||||||
|
|
||||||
|
if ($null -eq $dscValue) {
|
||||||
|
throw "No DSC value mapping for $shortKey = $numValue"
|
||||||
|
}
|
||||||
|
|
||||||
|
$dscSecOptions[$mapping.DscProperty] = $dscValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# DSC Configuration
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
Configuration DefaultDCPolicy
|
||||||
|
{
|
||||||
|
Import-DscResource -ModuleName SecurityPolicyDsc
|
||||||
|
|
||||||
|
Node 'localhost'
|
||||||
|
{
|
||||||
|
# ===============================================================
|
||||||
|
# User Rights Assignments (generated from settings.ps1)
|
||||||
|
# Force = $true means DSC controls the full list exclusively.
|
||||||
|
# Any identities not listed will be removed from the privilege.
|
||||||
|
# ===============================================================
|
||||||
|
foreach ($priv in $dscPrivileges) {
|
||||||
|
UserRightsAssignment $priv.ResourceName
|
||||||
|
{
|
||||||
|
Policy = $priv.Policy
|
||||||
|
Identity = $priv.Identity
|
||||||
|
Force = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===============================================================
|
||||||
|
# Security Options (generated from settings.ps1 Registry Values)
|
||||||
|
# ===============================================================
|
||||||
|
SecurityOption 'DCSecurityOptions'
|
||||||
|
{
|
||||||
|
Name = 'DCSecurityOptions'
|
||||||
|
Domain_controller_LDAP_server_signing_requirements = $dscSecOptions['Domain_controller_LDAP_server_signing_requirements']
|
||||||
|
Domain_member_Digitally_encrypt_or_sign_secure_channel_data_always = $dscSecOptions['Domain_member_Digitally_encrypt_or_sign_secure_channel_data_always']
|
||||||
|
Microsoft_network_server_Digitally_sign_communications_always = $dscSecOptions['Microsoft_network_server_Digitally_sign_communications_always']
|
||||||
|
Microsoft_network_server_Digitally_sign_communications_if_client_agrees = $dscSecOptions['Microsoft_network_server_Digitally_sign_communications_if_client_agrees']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
154
gpo/default-domain-controller/settings.ps1
Normal file
154
gpo/default-domain-controller/settings.ps1
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
# Default Domain Controllers Policy -- Settings Declaration
|
||||||
|
# GPO GUID: {6AC1786C-016F-11D2-945F-00C04FB984F9} (built-in, same on all domains)
|
||||||
|
# Linked to: OU=Domain Controllers,DC=example,DC=internal
|
||||||
|
#
|
||||||
|
# This GPO controls user rights assignments and security options for domain controllers.
|
||||||
|
# Privilege Rights use *SID notation as required by GptTmpl.inf format.
|
||||||
|
|
||||||
|
# Resolve custom group SIDs at evaluation time so they stay correct if groups are recreated
|
||||||
|
$masterAdminsSID = (Get-ADGroup -Identity 'MasterAdmins').SID.Value
|
||||||
|
|
||||||
|
@{
|
||||||
|
GPOName = 'Default Domain Controllers Policy'
|
||||||
|
Description = 'User rights assignments, security options, and signing requirements for domain controllers'
|
||||||
|
|
||||||
|
DisableUserConfiguration = $true
|
||||||
|
|
||||||
|
# Already linked at domain creation
|
||||||
|
LinkTo = $null
|
||||||
|
|
||||||
|
SecurityPolicy = @{
|
||||||
|
|
||||||
|
'Registry Values' = [ordered]@{
|
||||||
|
# LDAP server signing: 1=None (signing supported but not required)
|
||||||
|
'MACHINE\System\CurrentControlSet\Services\NTDS\Parameters\LDAPServerIntegrity' = '4,1'
|
||||||
|
# Secure channel: always encrypt or sign
|
||||||
|
'MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\RequireSignOrSeal' = '4,1'
|
||||||
|
# SMB server: always require signing
|
||||||
|
'MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\RequireSecuritySignature' = '4,1'
|
||||||
|
# SMB server: sign if client agrees
|
||||||
|
'MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\EnableSecuritySignature' = '4,1'
|
||||||
|
}
|
||||||
|
|
||||||
|
'Privilege Rights' = [ordered]@{
|
||||||
|
# --- Token and Process Privileges ---
|
||||||
|
|
||||||
|
# Replace a process-level token
|
||||||
|
SeAssignPrimaryTokenPrivilege = '*S-1-5-20,*S-1-5-19'
|
||||||
|
# NETWORK SERVICE, LOCAL SERVICE
|
||||||
|
|
||||||
|
# Generate security audits
|
||||||
|
SeAuditPrivilege = '*S-1-5-99-216390572-1995538116-3857911515-2404958512-2623887229,*S-1-5-20,*S-1-5-19'
|
||||||
|
# PrintSpoolerService, NETWORK SERVICE, LOCAL SERVICE
|
||||||
|
|
||||||
|
# Debug programs
|
||||||
|
SeDebugPrivilege = '*S-1-5-32-544'
|
||||||
|
# Administrators
|
||||||
|
|
||||||
|
# --- Backup and Restore ---
|
||||||
|
|
||||||
|
# Back up files and directories
|
||||||
|
SeBackupPrivilege = '*S-1-5-32-549,*S-1-5-32-551,*S-1-5-32-544'
|
||||||
|
# Server Operators, Backup Operators, Administrators
|
||||||
|
|
||||||
|
# Restore files and directories
|
||||||
|
SeRestorePrivilege = '*S-1-5-32-549,*S-1-5-32-551,*S-1-5-32-544'
|
||||||
|
# Server Operators, Backup Operators, Administrators
|
||||||
|
|
||||||
|
# --- Logon Rights ---
|
||||||
|
|
||||||
|
# Log on as a batch job
|
||||||
|
SeBatchLogonRight = '*S-1-5-32-559,*S-1-5-32-551,*S-1-5-32-544'
|
||||||
|
# Performance Log Users, Backup Operators, Administrators
|
||||||
|
|
||||||
|
# Allow log on locally
|
||||||
|
SeInteractiveLogonRight = '*S-1-5-9,*S-1-5-32-550,*S-1-5-32-549,*S-1-5-32-548,*S-1-5-32-551,*S-1-5-32-544'
|
||||||
|
# Enterprise DCs, Print Operators, Server Operators, Account Operators, Backup Operators, Administrators
|
||||||
|
|
||||||
|
# Allow log on through Remote Desktop Services
|
||||||
|
SeRemoteInteractiveLogonRight = "*S-1-5-32-544,*$masterAdminsSID"
|
||||||
|
# Administrators, MasterAdmins
|
||||||
|
|
||||||
|
# Access this computer from the network
|
||||||
|
SeNetworkLogonRight = '*S-1-5-32-554,*S-1-5-9,*S-1-5-11,*S-1-5-32-544,*S-1-1-0'
|
||||||
|
# Pre-Windows 2000 Compatible Access, Enterprise DCs, Authenticated Users, Administrators, Everyone
|
||||||
|
|
||||||
|
# Bypass traverse checking
|
||||||
|
SeChangeNotifyPrivilege = '*S-1-5-32-554,*S-1-5-11,*S-1-5-99-216390572-1995538116-3857911515-2404958512-2623887229,*S-1-5-32-544,*S-1-5-20,*S-1-5-19,*S-1-1-0'
|
||||||
|
# Pre-Windows 2000, Authenticated Users, PrintSpoolerService, Administrators, NETWORK SERVICE, LOCAL SERVICE, Everyone
|
||||||
|
|
||||||
|
# --- Domain and Machine Management ---
|
||||||
|
|
||||||
|
# Add workstations to domain
|
||||||
|
SeMachineAccountPrivilege = '*S-1-5-11'
|
||||||
|
# Authenticated Users
|
||||||
|
|
||||||
|
# Enable delegation
|
||||||
|
SeEnableDelegationPrivilege = '*S-1-5-32-544'
|
||||||
|
# Administrators
|
||||||
|
|
||||||
|
# --- System Privileges ---
|
||||||
|
|
||||||
|
# Create a pagefile
|
||||||
|
SeCreatePagefilePrivilege = '*S-1-5-32-544'
|
||||||
|
# Administrators
|
||||||
|
|
||||||
|
# Increase scheduling priority
|
||||||
|
SeIncreaseBasePriorityPrivilege = '*S-1-5-90-0,*S-1-5-32-544'
|
||||||
|
# Window Manager Group, Administrators
|
||||||
|
|
||||||
|
# Adjust memory quotas for a process
|
||||||
|
SeIncreaseQuotaPrivilege = '*S-1-5-32-544,*S-1-5-20,*S-1-5-19'
|
||||||
|
# Administrators, NETWORK SERVICE, LOCAL SERVICE
|
||||||
|
|
||||||
|
# Load and unload device drivers
|
||||||
|
SeLoadDriverPrivilege = '*S-1-5-32-550,*S-1-5-32-544'
|
||||||
|
# Print Operators, Administrators
|
||||||
|
|
||||||
|
# Profile single process
|
||||||
|
SeProfileSingleProcessPrivilege = '*S-1-5-32-544'
|
||||||
|
# Administrators
|
||||||
|
|
||||||
|
# Profile system performance
|
||||||
|
SeSystemProfilePrivilege = '*S-1-5-80-3139157870-2983391045-3678747466-658725712-1809340420,*S-1-5-32-544'
|
||||||
|
# WdiServiceHost, Administrators
|
||||||
|
|
||||||
|
# --- Shutdown ---
|
||||||
|
|
||||||
|
# Force shutdown from a remote system
|
||||||
|
SeRemoteShutdownPrivilege = '*S-1-5-32-549,*S-1-5-32-544'
|
||||||
|
# Server Operators, Administrators
|
||||||
|
|
||||||
|
# Shut down the system
|
||||||
|
SeShutdownPrivilege = '*S-1-5-32-550,*S-1-5-32-549,*S-1-5-32-551,*S-1-5-32-544'
|
||||||
|
# Print Operators, Server Operators, Backup Operators, Administrators
|
||||||
|
|
||||||
|
# --- Security and Audit ---
|
||||||
|
|
||||||
|
# Manage auditing and security log
|
||||||
|
SeSecurityPrivilege = '*S-1-5-32-544'
|
||||||
|
# Administrators
|
||||||
|
|
||||||
|
# Take ownership of files or other objects
|
||||||
|
SeTakeOwnershipPrivilege = '*S-1-5-32-544'
|
||||||
|
# Administrators
|
||||||
|
|
||||||
|
# --- Environment and Hardware ---
|
||||||
|
|
||||||
|
# Modify firmware environment values
|
||||||
|
SeSystemEnvironmentPrivilege = '*S-1-5-32-544'
|
||||||
|
# Administrators
|
||||||
|
|
||||||
|
# Change the system time
|
||||||
|
SeSystemTimePrivilege = '*S-1-5-32-549,*S-1-5-32-544,*S-1-5-19'
|
||||||
|
# Server Operators, Administrators, LOCAL SERVICE
|
||||||
|
|
||||||
|
# Remove computer from docking station
|
||||||
|
SeUndockPrivilege = '*S-1-5-32-544'
|
||||||
|
# Administrators
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# No registry-based (Administrative Template) settings in this GPO
|
||||||
|
RegistrySettings = @()
|
||||||
|
}
|
||||||
118
gpo/default-domain/DefaultDomainPolicy.ps1
Normal file
118
gpo/default-domain/DefaultDomainPolicy.ps1
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
# DefaultDomainPolicy.ps1
|
||||||
|
# DSC Configuration: Default Domain Policy for example.internal
|
||||||
|
#
|
||||||
|
# SINGLE SOURCE OF TRUTH: All values are read from settings.ps1.
|
||||||
|
# Do NOT hardcode policy values here -- edit settings.ps1 instead.
|
||||||
|
#
|
||||||
|
# When applied on a Domain Controller, these settings become the effective
|
||||||
|
# domain-wide policy -- matching what the Default Domain Policy GPO enforces.
|
||||||
|
|
||||||
|
# Load helpers for registry value parsing
|
||||||
|
. (Join-Path $PSScriptRoot '..\lib\GPOHelper.ps1')
|
||||||
|
|
||||||
|
# Load the authoritative settings
|
||||||
|
$domainSettings = & (Join-Path $PSScriptRoot 'settings.ps1')
|
||||||
|
$sysAccess = $domainSettings.SecurityPolicy['System Access']
|
||||||
|
$kerberosPolicy = $domainSettings.SecurityPolicy['Kerberos Policy']
|
||||||
|
$regValues = $domainSettings.SecurityPolicy['Registry Values']
|
||||||
|
|
||||||
|
# --- Value transforms ---
|
||||||
|
$toEnabledDisabled = { param($val) if ([int]$val -eq 1) { 'Enabled' } else { 'Disabled' } }
|
||||||
|
|
||||||
|
# Account Policy
|
||||||
|
$dscMinPasswordLength = [int]$sysAccess['MinimumPasswordLength']
|
||||||
|
$dscComplexity = & $toEnabledDisabled $sysAccess['PasswordComplexity']
|
||||||
|
$dscMaxPasswordAge = [int]$sysAccess['MaximumPasswordAge']
|
||||||
|
$dscMinPasswordAge = [int]$sysAccess['MinimumPasswordAge']
|
||||||
|
$dscPasswordHistory = [int]$sysAccess['PasswordHistorySize']
|
||||||
|
$dscReversibleEncryption = & $toEnabledDisabled $sysAccess['ClearTextPassword']
|
||||||
|
$dscLockoutThreshold = [int]$sysAccess['LockoutBadCount']
|
||||||
|
|
||||||
|
# Lockout duration/reset only meaningful when threshold > 0
|
||||||
|
$dscLockoutDuration = if ($dscLockoutThreshold -gt 0) { [int]$sysAccess['LockoutDuration'] } else { 0 }
|
||||||
|
$dscResetLockoutCount = if ($dscLockoutThreshold -gt 0) { [int]$sysAccess['ResetLockoutCount'] } else { 0 }
|
||||||
|
|
||||||
|
# Security Options
|
||||||
|
$dscForceLogoff = & $toEnabledDisabled $sysAccess['ForceLogoffWhenHourExpire']
|
||||||
|
$dscAnonNameLookup = & $toEnabledDisabled $sysAccess['LSAAnonymousNameLookup']
|
||||||
|
$noLmHashValue = Get-GptTmplRegValue $regValues['MACHINE\System\CurrentControlSet\Control\Lsa\NoLMHash']
|
||||||
|
$dscNoLmHash = & $toEnabledDisabled $noLmHashValue
|
||||||
|
|
||||||
|
Configuration DefaultDomainPolicy
|
||||||
|
{
|
||||||
|
Import-DscResource -ModuleName SecurityPolicyDsc
|
||||||
|
|
||||||
|
Node 'localhost'
|
||||||
|
{
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Account Policy (Password + Lockout)
|
||||||
|
# Only one AccountPolicy resource is allowed per configuration.
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
AccountPolicy 'DomainAccountPolicy'
|
||||||
|
{
|
||||||
|
Name = 'DomainAccountPolicy'
|
||||||
|
Minimum_Password_Length = $dscMinPasswordLength
|
||||||
|
Password_must_meet_complexity_requirements = $dscComplexity
|
||||||
|
Maximum_Password_Age = $dscMaxPasswordAge
|
||||||
|
Minimum_Password_Age = $dscMinPasswordAge
|
||||||
|
Enforce_password_history = $dscPasswordHistory
|
||||||
|
Store_passwords_using_reversible_encryption = $dscReversibleEncryption
|
||||||
|
Account_lockout_threshold = $dscLockoutThreshold
|
||||||
|
Account_lockout_duration = $dscLockoutDuration
|
||||||
|
Reset_account_lockout_counter_after = $dscResetLockoutCount
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Security Options
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
SecurityOption 'DomainSecurityOptions'
|
||||||
|
{
|
||||||
|
Name = 'DomainSecurityOptions'
|
||||||
|
Network_security_Do_not_store_LAN_Manager_hash_value_on_next_password_change = $dscNoLmHash
|
||||||
|
Network_security_Force_logoff_when_logon_hours_expire = $dscForceLogoff
|
||||||
|
Network_access_Allow_anonymous_SID_Name_translation = $dscAnonNameLookup
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Kerberos Policy (validated via secedit -- no DSC resource exists)
|
||||||
|
# SecurityPolicyDsc does not support Kerberos settings.
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
Script 'KerberosPolicy'
|
||||||
|
{
|
||||||
|
GetScript = { @{ Result = 'Kerberos policy validation' } }
|
||||||
|
|
||||||
|
SetScript = {
|
||||||
|
throw 'Kerberos policy remediation not supported via DSC. Use Apply-GPOBaseline.ps1 + gpupdate /force.'
|
||||||
|
}
|
||||||
|
|
||||||
|
TestScript = {
|
||||||
|
$tempFile = [System.IO.Path]::GetTempFileName()
|
||||||
|
try {
|
||||||
|
secedit /export /cfg $tempFile /quiet | Out-Null
|
||||||
|
$content = Get-Content $tempFile -Raw
|
||||||
|
|
||||||
|
$kerberosSettings = $using:kerberosPolicy
|
||||||
|
$compliant = $true
|
||||||
|
|
||||||
|
foreach ($key in $kerberosSettings.Keys) {
|
||||||
|
if ($content -match "$key\s*=\s*(\d+)") {
|
||||||
|
$actual = [int]$Matches[1]
|
||||||
|
$desired = [int]$kerberosSettings[$key]
|
||||||
|
if ($actual -ne $desired) {
|
||||||
|
Write-Verbose "Kerberos drift: $key = $actual (expected $desired)"
|
||||||
|
$compliant = $false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Verbose "Kerberos setting not found in local policy: $key"
|
||||||
|
$compliant = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $compliant
|
||||||
|
} finally {
|
||||||
|
Remove-Item $tempFile -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
gpo/default-domain/settings.ps1
Normal file
53
gpo/default-domain/settings.ps1
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Default Domain Policy -- Settings Declaration
|
||||||
|
# GPO GUID: {31B2F340-016D-11D2-945F-00C04FB984F9} (built-in, same on all domains)
|
||||||
|
# Linked to: example.internal (domain root)
|
||||||
|
#
|
||||||
|
# This GPO controls domain-wide password, account lockout, and Kerberos policies.
|
||||||
|
# These settings ONLY take effect at the domain level -- they are ignored in OU-level GPOs.
|
||||||
|
|
||||||
|
@{
|
||||||
|
GPOName = 'Default Domain Policy'
|
||||||
|
Description = 'Domain-wide password, account lockout, and Kerberos policies'
|
||||||
|
|
||||||
|
DisableUserConfiguration = $true
|
||||||
|
|
||||||
|
# No link management needed -- auto-linked at domain creation
|
||||||
|
LinkTo = $null
|
||||||
|
|
||||||
|
SecurityPolicy = @{
|
||||||
|
|
||||||
|
'System Access' = [ordered]@{
|
||||||
|
# --- Password Policy ---
|
||||||
|
MinimumPasswordAge = 1 # Days before password can be changed
|
||||||
|
MaximumPasswordAge = 42 # Days before password must be changed
|
||||||
|
MinimumPasswordLength = 7 # Minimum characters (consider 14+)
|
||||||
|
PasswordComplexity = 1 # 1=Enabled: requires 3 of 4 char types
|
||||||
|
PasswordHistorySize = 24 # Previous passwords remembered
|
||||||
|
ClearTextPassword = 0 # 0=Disabled: no reversible encryption
|
||||||
|
RequireLogonToChangePassword = 0 # 0=Disabled
|
||||||
|
ForceLogoffWhenHourExpire = 0 # 0=Disabled: sessions continue after hours expire
|
||||||
|
LSAAnonymousNameLookup = 0 # 0=Disabled: block anonymous SID-to-name resolution
|
||||||
|
|
||||||
|
# --- Account Lockout Policy ---
|
||||||
|
LockoutBadCount = 5 # Failed attempts before lockout
|
||||||
|
ResetLockoutCount = 30 # Minutes before counter resets
|
||||||
|
LockoutDuration = 30 # Minutes account stays locked
|
||||||
|
}
|
||||||
|
|
||||||
|
'Kerberos Policy' = [ordered]@{
|
||||||
|
MaxTicketAge = 10 # TGT lifetime in hours
|
||||||
|
MaxRenewAge = 7 # TGT max renewal in days
|
||||||
|
MaxServiceAge = 600 # Service ticket lifetime in minutes
|
||||||
|
MaxClockSkew = 5 # Max clock difference in minutes
|
||||||
|
TicketValidateClient = 1 # 1=Enabled: validate client identity
|
||||||
|
}
|
||||||
|
|
||||||
|
'Registry Values' = [ordered]@{
|
||||||
|
# Do not store LAN Manager hash -- LM hashes are trivially crackable
|
||||||
|
'MACHINE\System\CurrentControlSet\Control\Lsa\NoLMHash' = '4,1' # REG_DWORD=1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# No registry-based (Administrative Template) settings in this GPO
|
||||||
|
RegistrySettings = @()
|
||||||
|
}
|
||||||
272
gpo/lib/GPOAppLocker.ps1
Normal file
272
gpo/lib/GPOAppLocker.ps1
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
# GPOAppLocker.ps1
|
||||||
|
# AppLocker policy management via GPO.
|
||||||
|
# Uses Set-AppLockerPolicy / Get-AppLockerPolicy with -LDAP parameter.
|
||||||
|
# Depends on: GPOCore.ps1
|
||||||
|
|
||||||
|
function ConvertTo-AppLockerXml {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Converts an AppLockerPolicy hashtable into AppLocker XML format.
|
||||||
|
Generates unique GUIDs per rule and resolves user names to SIDs.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$AppLockerPolicy
|
||||||
|
)
|
||||||
|
|
||||||
|
$esc = [System.Security.SecurityElement]
|
||||||
|
|
||||||
|
$collectionMap = @{
|
||||||
|
Exe = 'Exe'
|
||||||
|
Msi = 'Msi'
|
||||||
|
Script = 'Script'
|
||||||
|
Appx = 'Appx'
|
||||||
|
Dll = 'Dll'
|
||||||
|
}
|
||||||
|
|
||||||
|
$collections = foreach ($collectionName in $AppLockerPolicy.Keys) {
|
||||||
|
$collection = $AppLockerPolicy[$collectionName]
|
||||||
|
$xmlType = $collectionMap[$collectionName]
|
||||||
|
if (-not $xmlType) {
|
||||||
|
Write-Host " [WARN] Unknown AppLocker collection: $collectionName" -ForegroundColor Yellow
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$enforcement = if ($collection.EnforcementMode -eq 'Enabled') {
|
||||||
|
'Enabled'
|
||||||
|
} else {
|
||||||
|
'AuditOnly'
|
||||||
|
}
|
||||||
|
|
||||||
|
$ruleXml = foreach ($rule in $collection.Rules) {
|
||||||
|
$ruleId = [Guid]::NewGuid().ToString()
|
||||||
|
$ruleName = if ($rule.Name) { $esc::Escape($rule.Name) } else { "$collectionName rule $ruleId" }
|
||||||
|
$ruleDesc = if ($rule.Description) { $esc::Escape($rule.Description) } else { '' }
|
||||||
|
$action = $rule.Action # Allow or Deny
|
||||||
|
|
||||||
|
# Resolve user to SID
|
||||||
|
$userSid = 'S-1-1-0' # Default: Everyone
|
||||||
|
try {
|
||||||
|
$ntAccount = New-Object System.Security.Principal.NTAccount($rule.User)
|
||||||
|
$userSid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
||||||
|
} catch {
|
||||||
|
# Well-known SIDs
|
||||||
|
if ($rule.User -eq 'Everyone') {
|
||||||
|
$userSid = 'S-1-1-0'
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARN] Cannot resolve '$($rule.User)' to SID, using Everyone" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$conditionXml = switch ($rule.Type) {
|
||||||
|
'Publisher' {
|
||||||
|
$pub = $esc::Escape($rule.Publisher)
|
||||||
|
$prod = if ($rule.Product) { $esc::Escape($rule.Product) } else { '*' }
|
||||||
|
$bin = if ($rule.Binary) { $esc::Escape($rule.Binary) } else { '*' }
|
||||||
|
$lowVer = if ($rule.LowVersion) { $rule.LowVersion } else { '0.0.0.0' }
|
||||||
|
$highVer = if ($rule.HighVersion) { $rule.HighVersion } else { '*' }
|
||||||
|
@"
|
||||||
|
<Conditions>
|
||||||
|
<FilePublisherCondition PublisherName="$pub" ProductName="$prod" BinaryName="$bin">
|
||||||
|
<BinaryVersionRange LowSection="$lowVer" HighSection="$highVer"/>
|
||||||
|
</FilePublisherCondition>
|
||||||
|
</Conditions>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
'Path' {
|
||||||
|
$path = $esc::Escape($rule.Path)
|
||||||
|
@"
|
||||||
|
<Conditions>
|
||||||
|
<FilePathCondition Path="$path"/>
|
||||||
|
</Conditions>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
'Hash' {
|
||||||
|
$hash = $rule.Hash
|
||||||
|
$fileName = if ($rule.FileName) { $esc::Escape($rule.FileName) } else { '' }
|
||||||
|
$fileLength = if ($rule.SourceFileLength) { $rule.SourceFileLength } else { '0' }
|
||||||
|
@"
|
||||||
|
<Conditions>
|
||||||
|
<FileHashCondition>
|
||||||
|
<FileHash Type="SHA256" Data="$hash" SourceFileName="$fileName" SourceFileLength="$fileLength"/>
|
||||||
|
</FileHashCondition>
|
||||||
|
</Conditions>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
Write-Host " [WARN] Unknown rule type: $($rule.Type)" -ForegroundColor Yellow
|
||||||
|
''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$elementName = switch ($rule.Type) {
|
||||||
|
'Publisher' { 'FilePublisherRule' }
|
||||||
|
'Path' { 'FilePathRule' }
|
||||||
|
'Hash' { 'FileHashRule' }
|
||||||
|
default { 'FilePublisherRule' }
|
||||||
|
}
|
||||||
|
|
||||||
|
@"
|
||||||
|
<$elementName Id="$ruleId" Name="$ruleName" Description="$ruleDesc" UserOrGroupSid="$userSid" Action="$action">
|
||||||
|
$conditionXml
|
||||||
|
</$elementName>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
@"
|
||||||
|
<RuleCollection Type="$xmlType" EnforcementMode="$enforcement">
|
||||||
|
$($ruleXml -join "`n")
|
||||||
|
</RuleCollection>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
return @"
|
||||||
|
<AppLockerPolicy Version="1">
|
||||||
|
$($collections -join "`n")
|
||||||
|
</AppLockerPolicy>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-GPOAppLockerPolicy {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Applies an AppLocker policy to a GPO using Set-AppLockerPolicy.
|
||||||
|
Full overwrite semantics.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$AppLockerPolicy,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host " Applying AppLocker policy..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$xml = ConvertTo-AppLockerXml -AppLockerPolicy $AppLockerPolicy
|
||||||
|
|
||||||
|
# Get GPO GUID for LDAP path
|
||||||
|
$gpo = Get-GPO -Name $GPOName -Domain $Domain
|
||||||
|
$gpoGuid = $gpo.Id.ToString('B').ToUpper()
|
||||||
|
$domainDN = (Get-ADDomain -Server $Domain).DistinguishedName
|
||||||
|
$ldapPath = "LDAP://CN=$gpoGuid,CN=Policies,CN=System,$domainDN"
|
||||||
|
|
||||||
|
# Write a temp file with the XML (Set-AppLockerPolicy requires -XmlPolicy file path or pipeline)
|
||||||
|
$tempFile = [System.IO.Path]::GetTempFileName()
|
||||||
|
try {
|
||||||
|
[System.IO.File]::WriteAllText($tempFile, $xml, [System.Text.UTF8Encoding]::new($true))
|
||||||
|
Set-AppLockerPolicy -XmlPolicy $tempFile -LDAP $ldapPath
|
||||||
|
Write-Host " [OK] AppLocker policy applied to $GPOName" -ForegroundColor Green
|
||||||
|
} finally {
|
||||||
|
Remove-Item $tempFile -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Report what was set
|
||||||
|
foreach ($coll in $AppLockerPolicy.Keys) {
|
||||||
|
$ruleCount = @($AppLockerPolicy[$coll].Rules).Count
|
||||||
|
$mode = $AppLockerPolicy[$coll].EnforcementMode
|
||||||
|
Write-Host " $coll`: $ruleCount rule(s), $mode" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-GPOAppLockerPolicy {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compares desired AppLocker policy against current GPO state.
|
||||||
|
Reports missing/extra collections and rules.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$AppLockerPolicy,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$diffs = @()
|
||||||
|
|
||||||
|
Write-Host " Comparing AppLocker policy..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Get GPO GUID for LDAP path
|
||||||
|
$gpo = Get-GPO -Name $GPOName -Domain $Domain
|
||||||
|
$gpoGuid = $gpo.Id.ToString('B').ToUpper()
|
||||||
|
$domainDN = (Get-ADDomain -Server $Domain).DistinguishedName
|
||||||
|
$ldapPath = "LDAP://CN=$gpoGuid,CN=Policies,CN=System,$domainDN"
|
||||||
|
|
||||||
|
# Get current AppLocker policy
|
||||||
|
$currentXml = $null
|
||||||
|
try {
|
||||||
|
$currentXml = Get-AppLockerPolicy -Domain -LDAP $ldapPath -Xml -ErrorAction Stop
|
||||||
|
} catch {
|
||||||
|
# No policy set
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $currentXml) {
|
||||||
|
Write-Host " [DRIFT] No AppLocker policy configured" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'AppLocker'
|
||||||
|
Collection = '(all)'
|
||||||
|
Status = 'No policy'
|
||||||
|
}
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse current XML
|
||||||
|
[xml]$currentDoc = $currentXml
|
||||||
|
|
||||||
|
foreach ($collectionName in $AppLockerPolicy.Keys) {
|
||||||
|
$desired = $AppLockerPolicy[$collectionName]
|
||||||
|
$desiredMode = if ($desired.EnforcementMode -eq 'Enabled') { 'Enabled' } else { 'AuditOnly' }
|
||||||
|
|
||||||
|
# Find matching collection in current XML
|
||||||
|
$currentCollection = $currentDoc.AppLockerPolicy.RuleCollection |
|
||||||
|
Where-Object { $_.Type -eq $collectionName }
|
||||||
|
|
||||||
|
if (-not $currentCollection) {
|
||||||
|
Write-Host " [DRIFT] Missing collection: $collectionName" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'AppLocker'
|
||||||
|
Collection = $collectionName
|
||||||
|
Status = 'Missing collection'
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check enforcement mode
|
||||||
|
if ($currentCollection.EnforcementMode -ne $desiredMode) {
|
||||||
|
Write-Host " [DRIFT] $collectionName enforcement: $($currentCollection.EnforcementMode) -> $desiredMode" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'AppLocker'
|
||||||
|
Collection = $collectionName
|
||||||
|
Status = "EnforcementMode: $($currentCollection.EnforcementMode) -> $desiredMode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compare rule counts
|
||||||
|
$currentRuleCount = @($currentCollection.ChildNodes | Where-Object { $_.LocalName -like '*Rule' }).Count
|
||||||
|
$desiredRuleCount = @($desired.Rules).Count
|
||||||
|
|
||||||
|
if ($currentRuleCount -ne $desiredRuleCount) {
|
||||||
|
Write-Host " [DRIFT] $collectionName rule count: $currentRuleCount -> $desiredRuleCount" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'AppLocker'
|
||||||
|
Collection = $collectionName
|
||||||
|
Status = "Rule count: $currentRuleCount -> $desiredRuleCount"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [OK] $collectionName`: $currentRuleCount rule(s), $($currentCollection.EnforcementMode)" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($diffs.Count -eq 0) {
|
||||||
|
Write-Host " [OK] AppLocker policy matches desired state" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [DRIFT] $($diffs.Count) AppLocker difference(s) found" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
217
gpo/lib/GPOAudit.ps1
Normal file
217
gpo/lib/GPOAudit.ps1
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
# GPOAudit.ps1
|
||||||
|
# Advanced Audit Policy (audit.csv) with subcategory GUID lookup table.
|
||||||
|
# Depends on: GPOCore.ps1 (Get-GPOSysvolPath, Add-GPOExtensionGuids)
|
||||||
|
|
||||||
|
# Well-known subcategory GUIDs -- static across all Windows versions.
|
||||||
|
$Script:AuditSubcategoryGuid = @{
|
||||||
|
# System
|
||||||
|
'Security State Change' = '{0CCE9210-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Security System Extension' = '{0CCE9211-69AE-11D9-BED3-505054503030}'
|
||||||
|
'System Integrity' = '{0CCE9212-69AE-11D9-BED3-505054503030}'
|
||||||
|
'IPsec Driver' = '{0CCE9213-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Other System Events' = '{0CCE9214-69AE-11D9-BED3-505054503030}'
|
||||||
|
|
||||||
|
# Logon/Logoff
|
||||||
|
'Logon' = '{0CCE9215-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Logoff' = '{0CCE9216-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Account Lockout' = '{0CCE9217-69AE-11D9-BED3-505054503030}'
|
||||||
|
'IPsec Main Mode' = '{0CCE9218-69AE-11D9-BED3-505054503030}'
|
||||||
|
'IPsec Quick Mode' = '{0CCE9219-69AE-11D9-BED3-505054503030}'
|
||||||
|
'IPsec Extended Mode' = '{0CCE921A-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Special Logon' = '{0CCE921B-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Other Logon/Logoff Events' = '{0CCE921C-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Network Policy Server' = '{0CCE9243-69AE-11D9-BED3-505054503030}'
|
||||||
|
'User / Device Claims' = '{0CCE9247-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Group Membership' = '{0CCE9249-69AE-11D9-BED3-505054503030}'
|
||||||
|
|
||||||
|
# Object Access
|
||||||
|
'File System' = '{0CCE921D-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Registry' = '{0CCE921E-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Kernel Object' = '{0CCE921F-69AE-11D9-BED3-505054503030}'
|
||||||
|
'SAM' = '{0CCE9220-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Certification Services' = '{0CCE9221-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Application Generated' = '{0CCE9222-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Handle Manipulation' = '{0CCE9223-69AE-11D9-BED3-505054503030}'
|
||||||
|
'File Share' = '{0CCE9224-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Filtering Platform Packet Drop' = '{0CCE9225-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Filtering Platform Connection' = '{0CCE9226-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Other Object Access Events' = '{0CCE9227-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Detailed File Share' = '{0CCE9244-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Removable Storage' = '{0CCE9245-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Central Policy Staging' = '{0CCE9246-69AE-11D9-BED3-505054503030}'
|
||||||
|
|
||||||
|
# Privilege Use
|
||||||
|
'Sensitive Privilege Use' = '{0CCE9228-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Non Sensitive Privilege Use' = '{0CCE9229-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Other Privilege Use Events' = '{0CCE922A-69AE-11D9-BED3-505054503030}'
|
||||||
|
|
||||||
|
# Detailed Tracking
|
||||||
|
'Process Creation' = '{0CCE922B-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Process Termination' = '{0CCE922C-69AE-11D9-BED3-505054503030}'
|
||||||
|
'DPAPI Activity' = '{0CCE922D-69AE-11D9-BED3-505054503030}'
|
||||||
|
'RPC Events' = '{0CCE922E-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Plug and Play Events' = '{0CCE9248-69AE-11D9-BED3-505054503030}'
|
||||||
|
|
||||||
|
# Policy Change
|
||||||
|
'Audit Policy Change' = '{0CCE922F-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Authentication Policy Change' = '{0CCE9230-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Authorization Policy Change' = '{0CCE9231-69AE-11D9-BED3-505054503030}'
|
||||||
|
'MPSSVC Rule-Level Policy Change' = '{0CCE9232-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Filtering Platform Policy Change' = '{0CCE9233-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Other Policy Change Events' = '{0CCE9234-69AE-11D9-BED3-505054503030}'
|
||||||
|
|
||||||
|
# Account Management
|
||||||
|
'User Account Management' = '{0CCE9235-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Computer Account Management' = '{0CCE9236-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Security Group Management' = '{0CCE9237-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Distribution Group Management' = '{0CCE9238-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Application Group Management' = '{0CCE9239-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Other Account Management Events' = '{0CCE923A-69AE-11D9-BED3-505054503030}'
|
||||||
|
|
||||||
|
# DS Access
|
||||||
|
'Directory Service Access' = '{0CCE923B-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Directory Service Changes' = '{0CCE923C-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Directory Service Replication' = '{0CCE923D-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Detailed Directory Service Replication' = '{0CCE923E-69AE-11D9-BED3-505054503030}'
|
||||||
|
|
||||||
|
# Account Logon
|
||||||
|
'Credential Validation' = '{0CCE923F-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Kerberos Service Ticket Operations' = '{0CCE9240-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Other Account Logon Events' = '{0CCE9241-69AE-11D9-BED3-505054503030}'
|
||||||
|
'Kerberos Authentication Service' = '{0CCE9242-69AE-11D9-BED3-505054503030}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Maps human-readable setting strings to numeric values for audit.csv
|
||||||
|
$Script:AuditSettingValue = @{
|
||||||
|
'No Auditing' = 0
|
||||||
|
'Success' = 1
|
||||||
|
'Failure' = 2
|
||||||
|
'Success and Failure' = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-GPOAdvancedAuditPolicy {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Writes an Advanced Audit Policy (audit.csv) to a GPO's SYSVOL path.
|
||||||
|
This provides subcategory-level audit control (53 subcategories)
|
||||||
|
instead of the 9 legacy Event Audit categories.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$AuditPolicy,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
|
||||||
|
$auditDir = Join-Path $sysvolPath 'Machine\Microsoft\Windows NT\Audit'
|
||||||
|
$auditCsvPath = Join-Path $auditDir 'audit.csv'
|
||||||
|
|
||||||
|
if (-not (Test-Path $auditDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $auditDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build CSV content
|
||||||
|
$sb = [System.Text.StringBuilder]::new()
|
||||||
|
[void]$sb.AppendLine('Machine Name,Policy Target,Subcategory,Subcategory GUID,Inclusion Setting,Exclusion Setting,Setting Value')
|
||||||
|
|
||||||
|
foreach ($subcategory in $AuditPolicy.Keys) {
|
||||||
|
$guid = $Script:AuditSubcategoryGuid[$subcategory]
|
||||||
|
if (-not $guid) {
|
||||||
|
Write-Host " [WARN] Unknown audit subcategory: '$subcategory'" -ForegroundColor Yellow
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$settingName = $AuditPolicy[$subcategory]
|
||||||
|
$settingValue = $Script:AuditSettingValue[$settingName]
|
||||||
|
if ($null -eq $settingValue) {
|
||||||
|
Write-Host " [WARN] Unknown audit setting value: '$settingName' for '$subcategory'" -ForegroundColor Yellow
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
[void]$sb.AppendLine(",System,$subcategory,$guid,$settingName,,$settingValue")
|
||||||
|
}
|
||||||
|
|
||||||
|
# audit.csv uses UTF-8 with BOM
|
||||||
|
$utf8Bom = [System.Text.UTF8Encoding]::new($true)
|
||||||
|
[System.IO.File]::WriteAllText($auditCsvPath, $sb.ToString(), $utf8Bom)
|
||||||
|
Write-Host " Written: $auditCsvPath" -ForegroundColor Green
|
||||||
|
Write-Host " $($AuditPolicy.Count) audit subcategory setting(s) configured." -ForegroundColor Green
|
||||||
|
|
||||||
|
# Register Audit Policy CSE GUID
|
||||||
|
$auditCseGuid = '{F3BC9527-C350-4C90-861C-1EC90034520B}'
|
||||||
|
$auditToolGuid = '{D02B1F72-3407-48AE-BA88-E8213C6761F1}'
|
||||||
|
Add-GPOExtensionGuids -GPOName $GPOName -CseGuid $auditCseGuid -ToolGuid $auditToolGuid -Scope Machine -Domain $Domain
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-GPOAdvancedAuditPolicy {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compares desired Advanced Audit Policy settings against the current
|
||||||
|
audit.csv in SYSVOL. Returns diff objects for mismatches.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$AuditPolicy,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
|
||||||
|
$auditCsvPath = Join-Path $sysvolPath 'Machine\Microsoft\Windows NT\Audit\audit.csv'
|
||||||
|
|
||||||
|
$diffs = @()
|
||||||
|
|
||||||
|
Write-Host " Comparing advanced audit policy..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Parse existing audit.csv into a lookup
|
||||||
|
$currentSettings = @{}
|
||||||
|
if (Test-Path $auditCsvPath) {
|
||||||
|
$csvLines = [System.IO.File]::ReadAllLines($auditCsvPath, [System.Text.UTF8Encoding]::new($true))
|
||||||
|
foreach ($line in $csvLines) {
|
||||||
|
# Skip header and empty lines
|
||||||
|
if ($line -match '^Machine Name,' -or [string]::IsNullOrWhiteSpace($line)) { continue }
|
||||||
|
|
||||||
|
$fields = $line -split ','
|
||||||
|
if ($fields.Count -ge 5) {
|
||||||
|
$subcategory = $fields[2].Trim()
|
||||||
|
$inclusionSetting = $fields[4].Trim()
|
||||||
|
$currentSettings[$subcategory] = $inclusionSetting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($subcategory in $AuditPolicy.Keys) {
|
||||||
|
$guid = $Script:AuditSubcategoryGuid[$subcategory]
|
||||||
|
if (-not $guid) { continue }
|
||||||
|
|
||||||
|
$desired = $AuditPolicy[$subcategory]
|
||||||
|
$current = $currentSettings[$subcategory]
|
||||||
|
|
||||||
|
if ($current -ne $desired) {
|
||||||
|
$currentDisplay = if ($null -eq $current) { '(not set)' } else { $current }
|
||||||
|
Write-Host " [DRIFT] $subcategory`: '$currentDisplay' -> '$desired'" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'AdvancedAudit'
|
||||||
|
Subcategory = $subcategory
|
||||||
|
Current = $currentDisplay
|
||||||
|
Desired = $desired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($diffs.Count -eq 0) {
|
||||||
|
Write-Host " [OK] Advanced audit policy matches desired state" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [DRIFT] $($diffs.Count) audit policy difference(s) found" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
214
gpo/lib/GPOBackup.ps1
Normal file
214
gpo/lib/GPOBackup.ps1
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
# GPOBackup.ps1
|
||||||
|
# GPO state backup and restore functions.
|
||||||
|
# Depends on: GPOCore.ps1 (Get-GPOSysvolPath, Update-GPOVersion)
|
||||||
|
|
||||||
|
$Script:DefaultBackupRoot = Join-Path $PSScriptRoot '..\backups'
|
||||||
|
$Script:DefaultRetention = 5
|
||||||
|
|
||||||
|
function Backup-GPOState {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Snapshots a GPO's SYSVOL directory tree and AD attributes to a
|
||||||
|
timestamped backup directory.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[string]$BackupRoot = $Script:DefaultBackupRoot,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$gpo = Get-GPO -Name $GPOName -Domain $Domain -ErrorAction Stop
|
||||||
|
$gpoGuid = "{$($gpo.Id)}"
|
||||||
|
$domainDN = (Get-ADDomain -Server $Domain).DistinguishedName
|
||||||
|
$gpoDN = "CN=$gpoGuid,CN=Policies,CN=System,$domainDN"
|
||||||
|
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
|
||||||
|
|
||||||
|
# Create timestamped backup directory
|
||||||
|
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||||
|
$safeName = $GPOName -replace '[^\w\-]', '_'
|
||||||
|
$backupDir = Join-Path $BackupRoot "$safeName\$timestamp"
|
||||||
|
|
||||||
|
if (-not (Test-Path $backupDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $backupDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Snapshot SYSVOL ---
|
||||||
|
# robocopy handles empty directories and UNC paths reliably;
|
||||||
|
# Copy-Item -Recurse fails on empty dirs in PowerShell 5.1.
|
||||||
|
$sysvolBackup = Join-Path $backupDir 'sysvol'
|
||||||
|
if (Test-Path $sysvolPath) {
|
||||||
|
$null = robocopy $sysvolPath $sysvolBackup /E /R:0 /W:0 /NJH /NJS /NFL /NDL /NP
|
||||||
|
Write-Host " [BACKUP] SYSVOL files captured" -ForegroundColor Cyan
|
||||||
|
} else {
|
||||||
|
New-Item -ItemType Directory -Path $sysvolBackup -Force | Out-Null
|
||||||
|
Write-Host " [BACKUP] No SYSVOL content to back up" -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Snapshot AD attributes ---
|
||||||
|
$adObj = Get-ADObject $gpoDN -Properties `
|
||||||
|
versionNumber, description, gPCMachineExtensionNames, `
|
||||||
|
gPCUserExtensionNames, gPCWQLFilter
|
||||||
|
|
||||||
|
$metadata = @{
|
||||||
|
GPOName = $GPOName
|
||||||
|
GPOGuid = $gpoGuid
|
||||||
|
Timestamp = (Get-Date).ToString('o')
|
||||||
|
Admin = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||||
|
Domain = $Domain
|
||||||
|
VersionNumber = $adObj.versionNumber
|
||||||
|
Description = $adObj.description
|
||||||
|
gPCMachineExtensionNames = $adObj.gPCMachineExtensionNames
|
||||||
|
gPCUserExtensionNames = $adObj.gPCUserExtensionNames
|
||||||
|
gPCWQLFilter = $adObj.gPCWQLFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadataPath = Join-Path $backupDir 'metadata.json'
|
||||||
|
$metadata | ConvertTo-Json -Depth 4 | Set-Content -Path $metadataPath -Encoding UTF8
|
||||||
|
Write-Host " [BACKUP] AD metadata captured -> $timestamp" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# --- Enforce retention ---
|
||||||
|
Invoke-GPOBackupRetention -GPOName $GPOName -BackupRoot $BackupRoot
|
||||||
|
|
||||||
|
return $backupDir
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-GPOBackupRetention {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Removes old backups beyond the retention limit (default 5).
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[string]$BackupRoot = $Script:DefaultBackupRoot,
|
||||||
|
|
||||||
|
[int]$RetentionCount = $Script:DefaultRetention
|
||||||
|
)
|
||||||
|
|
||||||
|
$safeName = $GPOName -replace '[^\w\-]', '_'
|
||||||
|
$gpoBackupDir = Join-Path $BackupRoot $safeName
|
||||||
|
|
||||||
|
if (-not (Test-Path $gpoBackupDir)) { return }
|
||||||
|
|
||||||
|
$backups = Get-ChildItem -Path $gpoBackupDir -Directory | Sort-Object Name -Descending
|
||||||
|
if ($backups.Count -gt $RetentionCount) {
|
||||||
|
$toRemove = $backups | Select-Object -Skip $RetentionCount
|
||||||
|
foreach ($old in $toRemove) {
|
||||||
|
Remove-Item -Path $old.FullName -Recurse -Force
|
||||||
|
Write-Host " [BACKUP] Pruned old backup: $($old.Name)" -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-GPOBackups {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Lists available backups for a GPO (or all GPOs if no name specified).
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[string]$BackupRoot = $Script:DefaultBackupRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-Path $BackupRoot)) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = @()
|
||||||
|
|
||||||
|
if ($GPOName) {
|
||||||
|
$safeName = $GPOName -replace '[^\w\-]', '_'
|
||||||
|
$searchPath = Join-Path $BackupRoot $safeName
|
||||||
|
if (-not (Test-Path $searchPath)) { return @() }
|
||||||
|
$gpoDirs = @(Get-Item $searchPath)
|
||||||
|
} else {
|
||||||
|
$gpoDirs = Get-ChildItem -Path $BackupRoot -Directory
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($gpoDir in $gpoDirs) {
|
||||||
|
$backups = Get-ChildItem -Path $gpoDir.FullName -Directory | Sort-Object Name -Descending
|
||||||
|
foreach ($backup in $backups) {
|
||||||
|
$metadataPath = Join-Path $backup.FullName 'metadata.json'
|
||||||
|
if (Test-Path $metadataPath) {
|
||||||
|
$meta = Get-Content $metadataPath -Raw | ConvertFrom-Json
|
||||||
|
$results += [PSCustomObject]@{
|
||||||
|
GPOName = $meta.GPOName
|
||||||
|
Timestamp = $backup.Name
|
||||||
|
Admin = $meta.Admin
|
||||||
|
Version = $meta.VersionNumber
|
||||||
|
Path = $backup.FullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results
|
||||||
|
}
|
||||||
|
|
||||||
|
function Restore-GPOState {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Restores a GPO's SYSVOL files and AD attributes from a backup.
|
||||||
|
.DESCRIPTION
|
||||||
|
Copies backed-up SYSVOL content back to the live SYSVOL path and
|
||||||
|
restores AD attributes (version, extensions, description, WMI filter link).
|
||||||
|
Bumps the version number to force clients to reprocess.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$BackupPath,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$metadataPath = Join-Path $BackupPath 'metadata.json'
|
||||||
|
if (-not (Test-Path $metadataPath)) {
|
||||||
|
throw "Backup metadata not found at $BackupPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = Get-Content $metadataPath -Raw | ConvertFrom-Json
|
||||||
|
|
||||||
|
$gpoName = $meta.GPOName
|
||||||
|
$gpo = Get-GPO -Name $gpoName -Domain $Domain -ErrorAction Stop
|
||||||
|
$gpoGuid = "{$($gpo.Id)}"
|
||||||
|
$domainDN = (Get-ADDomain -Server $Domain).DistinguishedName
|
||||||
|
$gpoDN = "CN=$gpoGuid,CN=Policies,CN=System,$domainDN"
|
||||||
|
$sysvolPath = Get-GPOSysvolPath -GPOName $gpoName -Domain $Domain
|
||||||
|
|
||||||
|
Write-Host "Restoring GPO: $gpoName from backup $($meta.Timestamp)" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# --- Restore SYSVOL ---
|
||||||
|
$sysvolBackup = Join-Path $BackupPath 'sysvol'
|
||||||
|
if (Test-Path $sysvolBackup) {
|
||||||
|
$backedUpContent = Get-ChildItem $sysvolBackup
|
||||||
|
if ($backedUpContent.Count -gt 0) {
|
||||||
|
$null = robocopy $sysvolBackup $sysvolPath /E /R:0 /W:0 /NJH /NJS /NFL /NDL /NP
|
||||||
|
Write-Host " [RESTORED] SYSVOL files" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [RESTORED] No SYSVOL content in backup (empty)" -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Restore AD attributes ---
|
||||||
|
$replaceAttrs = @{}
|
||||||
|
if ($meta.Description) { $replaceAttrs['description'] = $meta.Description }
|
||||||
|
if ($meta.gPCMachineExtensionNames) { $replaceAttrs['gPCMachineExtensionNames'] = $meta.gPCMachineExtensionNames }
|
||||||
|
if ($meta.gPCUserExtensionNames) { $replaceAttrs['gPCUserExtensionNames'] = $meta.gPCUserExtensionNames }
|
||||||
|
if ($meta.gPCWQLFilter) { $replaceAttrs['gPCWQLFilter'] = $meta.gPCWQLFilter }
|
||||||
|
|
||||||
|
if ($replaceAttrs.Count -gt 0) {
|
||||||
|
Set-ADObject $gpoDN -Replace $replaceAttrs
|
||||||
|
Write-Host " [RESTORED] AD attributes" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Bump version to force client reprocessing ---
|
||||||
|
Update-GPOVersion -GPOName $gpoName -Scope Both -Domain $Domain
|
||||||
|
Write-Host " [RESTORED] Version bumped (clients will reprocess)" -ForegroundColor Green
|
||||||
|
|
||||||
|
Write-Host "Restore complete for $gpoName." -ForegroundColor Green
|
||||||
|
}
|
||||||
273
gpo/lib/GPOCore.ps1
Normal file
273
gpo/lib/GPOCore.ps1
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
# GPOCore.ps1
|
||||||
|
# Shared utility functions used by multiple GPO modules.
|
||||||
|
# Must be loaded before other GPO modules (they depend on these functions).
|
||||||
|
|
||||||
|
function Get-GPOSysvolPath {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Resolves a GPO name to its SYSVOL filesystem path.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$gpo = Get-GPO -Name $GPOName -ErrorAction Stop
|
||||||
|
$guid = "{$($gpo.Id)}"
|
||||||
|
return "\\$Domain\SYSVOL\$Domain\Policies\$guid"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-GPOSecurityTemplatePath {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Returns the full path to a GPO's GptTmpl.inf file.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
|
||||||
|
return Join-Path $sysvolPath 'MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-GPOVersion {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Bumps a GPO's version number in both AD and GPT.INI.
|
||||||
|
.DESCRIPTION
|
||||||
|
The GPO version number is a packed 32-bit integer:
|
||||||
|
- Upper 16 bits = user configuration version
|
||||||
|
- Lower 16 bits = machine configuration version
|
||||||
|
This function increments only the relevant half based on -Scope.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[ValidateSet('Machine', 'User', 'Both')]
|
||||||
|
[string]$Scope = 'Machine',
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$gpo = Get-GPO -Name $GPOName -Domain $Domain
|
||||||
|
$gpoGuid = "{$($gpo.Id)}"
|
||||||
|
$gpoDN = "CN=$gpoGuid,CN=Policies,CN=System,$((Get-ADDomain -Server $Domain).DistinguishedName)"
|
||||||
|
|
||||||
|
# Read current packed version and split into halves
|
||||||
|
$adVersion = [int](Get-ADObject $gpoDN -Properties versionNumber).versionNumber
|
||||||
|
$machineVer = $adVersion -band 0xFFFF
|
||||||
|
$userVer = ($adVersion -shr 16) -band 0xFFFF
|
||||||
|
|
||||||
|
switch ($Scope) {
|
||||||
|
'Machine' { $machineVer++ }
|
||||||
|
'User' { $userVer++ }
|
||||||
|
'Both' { $machineVer++; $userVer++ }
|
||||||
|
}
|
||||||
|
|
||||||
|
$newVersion = ($userVer -shl 16) -bor $machineVer
|
||||||
|
Set-ADObject $gpoDN -Replace @{ versionNumber = $newVersion }
|
||||||
|
|
||||||
|
# GPT.INI uses the same packed format
|
||||||
|
$gptIniPath = Join-Path (Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain) 'GPT.INI'
|
||||||
|
if (Test-Path $gptIniPath) {
|
||||||
|
$gptContent = Get-Content $gptIniPath -Raw
|
||||||
|
if ($gptContent -match 'Version=(\d+)') {
|
||||||
|
$oldVer = [int]$Matches[1]
|
||||||
|
$oldMachine = $oldVer -band 0xFFFF
|
||||||
|
$oldUser = ($oldVer -shr 16) -band 0xFFFF
|
||||||
|
|
||||||
|
switch ($Scope) {
|
||||||
|
'Machine' { $oldMachine++ }
|
||||||
|
'User' { $oldUser++ }
|
||||||
|
'Both' { $oldMachine++; $oldUser++ }
|
||||||
|
}
|
||||||
|
|
||||||
|
$newGptVer = ($oldUser -shl 16) -bor $oldMachine
|
||||||
|
$gptContent = $gptContent -replace "Version=$oldVer", "Version=$newGptVer"
|
||||||
|
Set-Content -Path $gptIniPath -Value $gptContent -NoNewline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-GPOExtensionGuids {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Ensures the specified CSE/tool GUID pair is present in a GPO's
|
||||||
|
extension name attributes (gPCMachineExtensionNames or gPCUserExtensionNames).
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$CseGuid,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$ToolGuid,
|
||||||
|
|
||||||
|
[ValidateSet('Machine', 'User')]
|
||||||
|
[string]$Scope = 'Machine',
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$gpo = Get-GPO -Name $GPOName -Domain $Domain
|
||||||
|
$gpoGuid = "{$($gpo.Id)}"
|
||||||
|
$gpoDN = "CN=$gpoGuid,CN=Policies,CN=System,$((Get-ADDomain -Server $Domain).DistinguishedName)"
|
||||||
|
|
||||||
|
$attrName = if ($Scope -eq 'Machine') { 'gPCMachineExtensionNames' } else { 'gPCUserExtensionNames' }
|
||||||
|
$obj = Get-ADObject $gpoDN -Properties $attrName
|
||||||
|
$current = $obj.$attrName
|
||||||
|
if (-not $current) { $current = '' }
|
||||||
|
|
||||||
|
$pair = "[$CseGuid$ToolGuid]"
|
||||||
|
if ($current.Contains($pair)) { return }
|
||||||
|
|
||||||
|
# Parse existing pairs, add new one, sort by CSE GUID
|
||||||
|
$pairs = [System.Collections.Generic.List[string]]::new()
|
||||||
|
$regex = [regex]'\[\{[0-9A-Fa-f-]+\}\{[0-9A-Fa-f-]+\}\]'
|
||||||
|
foreach ($m in $regex.Matches($current)) {
|
||||||
|
$pairs.Add($m.Value)
|
||||||
|
}
|
||||||
|
$pairs.Add($pair)
|
||||||
|
$sorted = $pairs | Sort-Object
|
||||||
|
|
||||||
|
$newValue = -join $sorted
|
||||||
|
Set-ADObject $gpoDN -Replace @{ $attrName = $newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-GPOStatus {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compares the desired GPO status (enabled/disabled sections) against
|
||||||
|
the current state. Returns a diff object if they differ.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[bool]$DisableUserConfiguration = $false,
|
||||||
|
[bool]$DisableComputerConfiguration = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
$gpo = Get-GPO -Name $GPOName -ErrorAction Stop
|
||||||
|
|
||||||
|
$desiredStatus = 'AllSettingsEnabled'
|
||||||
|
if ($DisableUserConfiguration -and $DisableComputerConfiguration) {
|
||||||
|
$desiredStatus = 'AllSettingsDisabled'
|
||||||
|
} elseif ($DisableUserConfiguration) {
|
||||||
|
$desiredStatus = 'UserSettingsDisabled'
|
||||||
|
} elseif ($DisableComputerConfiguration) {
|
||||||
|
$desiredStatus = 'ComputerSettingsDisabled'
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentStatus = $gpo.GpoStatus.ToString()
|
||||||
|
|
||||||
|
if ($currentStatus -ne $desiredStatus) {
|
||||||
|
Write-Host " [DRIFT] GpoStatus: '$currentStatus' -> '$desiredStatus'" -ForegroundColor Red
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Type = 'GpoStatus'
|
||||||
|
GPO = $GPOName
|
||||||
|
Current = $currentStatus
|
||||||
|
Desired = $desiredStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host " [OK] GpoStatus: $currentStatus" -ForegroundColor Green
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-GPOStatus {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Sets the GPO status (enabled/disabled Computer/User configuration).
|
||||||
|
Uses the GPO object's GpoStatus property which sets gPCOptions internally.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[bool]$DisableUserConfiguration = $false,
|
||||||
|
[bool]$DisableComputerConfiguration = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
$gpo = Get-GPO -Name $GPOName -ErrorAction Stop
|
||||||
|
|
||||||
|
$desiredStatus = 'AllSettingsEnabled'
|
||||||
|
if ($DisableUserConfiguration -and $DisableComputerConfiguration) {
|
||||||
|
$desiredStatus = 'AllSettingsDisabled'
|
||||||
|
} elseif ($DisableUserConfiguration) {
|
||||||
|
$desiredStatus = 'UserSettingsDisabled'
|
||||||
|
} elseif ($DisableComputerConfiguration) {
|
||||||
|
$desiredStatus = 'ComputerSettingsDisabled'
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentStatus = $gpo.GpoStatus.ToString()
|
||||||
|
if ($currentStatus -ne $desiredStatus) {
|
||||||
|
$gpo.GpoStatus = [Microsoft.GroupPolicy.GpoStatus]::$desiredStatus
|
||||||
|
Write-Host " [UPDATED] GpoStatus: $currentStatus -> $desiredStatus" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# DSC Helper Functions
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
|
||||||
|
function Resolve-SIDsToNames {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Translates a GptTmpl.inf SID string into an array of NTAccount names.
|
||||||
|
.EXAMPLE
|
||||||
|
Resolve-SIDsToNames '*S-1-5-32-544,*S-1-5-20'
|
||||||
|
# Returns: @('BUILTIN\Administrators', 'NT AUTHORITY\NETWORK SERVICE')
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$SIDString
|
||||||
|
)
|
||||||
|
|
||||||
|
$sids = $SIDString -split ',' | ForEach-Object { $_.Trim().TrimStart('*') }
|
||||||
|
$names = @()
|
||||||
|
|
||||||
|
foreach ($sidStr in $sids) {
|
||||||
|
try {
|
||||||
|
$sid = New-Object System.Security.Principal.SecurityIdentifier($sidStr)
|
||||||
|
$account = $sid.Translate([System.Security.Principal.NTAccount])
|
||||||
|
$names += $account.Value
|
||||||
|
} catch {
|
||||||
|
# If translation fails (e.g., virtual service account), keep the raw SID
|
||||||
|
Write-Warning "Could not resolve SID '$sidStr' to a name. Using raw SID."
|
||||||
|
$names += $sidStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $names
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-GptTmplRegValue {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Parses a GptTmpl.inf Registry Values entry and returns the numeric value.
|
||||||
|
.DESCRIPTION
|
||||||
|
GptTmpl.inf stores registry values as 'type,value' (e.g., '4,1' = REG_DWORD 1).
|
||||||
|
This function strips the type prefix and returns the value as an integer.
|
||||||
|
.EXAMPLE
|
||||||
|
Get-GptTmplRegValue '4,1' # Returns: 1
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$RegValueString
|
||||||
|
)
|
||||||
|
|
||||||
|
$parts = $RegValueString -split ',', 2
|
||||||
|
if ($parts.Count -lt 2) {
|
||||||
|
throw "Invalid GptTmpl.inf registry value format: '$RegValueString'. Expected 'type,value'."
|
||||||
|
}
|
||||||
|
|
||||||
|
return [int]$parts[1]
|
||||||
|
}
|
||||||
303
gpo/lib/GPOFirewall.ps1
Normal file
303
gpo/lib/GPOFirewall.ps1
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
# GPOFirewall.ps1
|
||||||
|
# Windows Firewall rule and profile management via GPO.
|
||||||
|
# Uses Open-NetGPO / New-NetFirewallRule -GPOSession / Save-NetGPO cmdlets.
|
||||||
|
# Depends on: GPOCore.ps1 (Get-GPOSysvolPath)
|
||||||
|
|
||||||
|
function Set-GPOFirewallProfiles {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Sets Windows Firewall profile settings (Domain/Private/Public) on a GPO
|
||||||
|
via registry values.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$FirewallProfiles,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$profileKeyMap = @{
|
||||||
|
Domain = 'HKLM\Software\Policies\Microsoft\WindowsFirewall\DomainProfile'
|
||||||
|
Private = 'HKLM\Software\Policies\Microsoft\WindowsFirewall\PrivateProfile'
|
||||||
|
Public = 'HKLM\Software\Policies\Microsoft\WindowsFirewall\PublicProfile'
|
||||||
|
}
|
||||||
|
|
||||||
|
$actionMap = @{
|
||||||
|
'Block' = 1
|
||||||
|
'Allow' = 0
|
||||||
|
'NotConfigured' = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($profileName in $FirewallProfiles.Keys) {
|
||||||
|
$profile = $FirewallProfiles[$profileName]
|
||||||
|
$regKey = $profileKeyMap[$profileName]
|
||||||
|
if (-not $regKey) {
|
||||||
|
Write-Host " [WARN] Unknown firewall profile: $profileName" -ForegroundColor Yellow
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enable/Disable firewall for this profile
|
||||||
|
if ($profile.ContainsKey('Enabled')) {
|
||||||
|
$enableValue = if ($profile.Enabled) { 1 } else { 0 }
|
||||||
|
Set-GPRegistryValue -Name $GPOName -Key $regKey -ValueName 'EnableFirewall' `
|
||||||
|
-Type DWord -Value $enableValue -Domain $Domain | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default inbound action
|
||||||
|
if ($profile.ContainsKey('DefaultInboundAction')) {
|
||||||
|
$inValue = $actionMap[$profile.DefaultInboundAction]
|
||||||
|
Set-GPRegistryValue -Name $GPOName -Key $regKey -ValueName 'DefaultInboundAction' `
|
||||||
|
-Type DWord -Value $inValue -Domain $Domain | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default outbound action
|
||||||
|
if ($profile.ContainsKey('DefaultOutboundAction')) {
|
||||||
|
$outValue = $actionMap[$profile.DefaultOutboundAction]
|
||||||
|
Set-GPRegistryValue -Name $GPOName -Key $regKey -ValueName 'DefaultOutboundAction' `
|
||||||
|
-Type DWord -Value $outValue -Domain $Domain | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host " [SET] Firewall profile: $profileName" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-GPOFirewallProfiles {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compares desired firewall profile settings against current GPO state.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$FirewallProfiles,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$profileKeyMap = @{
|
||||||
|
Domain = 'HKLM\Software\Policies\Microsoft\WindowsFirewall\DomainProfile'
|
||||||
|
Private = 'HKLM\Software\Policies\Microsoft\WindowsFirewall\PrivateProfile'
|
||||||
|
Public = 'HKLM\Software\Policies\Microsoft\WindowsFirewall\PublicProfile'
|
||||||
|
}
|
||||||
|
|
||||||
|
$actionMap = @{
|
||||||
|
'Block' = 1
|
||||||
|
'Allow' = 0
|
||||||
|
'NotConfigured' = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$diffs = @()
|
||||||
|
|
||||||
|
Write-Host " Comparing firewall profiles..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
foreach ($profileName in $FirewallProfiles.Keys) {
|
||||||
|
$profile = $FirewallProfiles[$profileName]
|
||||||
|
$regKey = $profileKeyMap[$profileName]
|
||||||
|
if (-not $regKey) { continue }
|
||||||
|
|
||||||
|
$settingsToCheck = @()
|
||||||
|
|
||||||
|
if ($profile.ContainsKey('Enabled')) {
|
||||||
|
$desired = if ($profile.Enabled) { 1 } else { 0 }
|
||||||
|
$settingsToCheck += @{ ValueName = 'EnableFirewall'; Desired = $desired }
|
||||||
|
}
|
||||||
|
if ($profile.ContainsKey('DefaultInboundAction')) {
|
||||||
|
$settingsToCheck += @{ ValueName = 'DefaultInboundAction'; Desired = $actionMap[$profile.DefaultInboundAction] }
|
||||||
|
}
|
||||||
|
if ($profile.ContainsKey('DefaultOutboundAction')) {
|
||||||
|
$settingsToCheck += @{ ValueName = 'DefaultOutboundAction'; Desired = $actionMap[$profile.DefaultOutboundAction] }
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($setting in $settingsToCheck) {
|
||||||
|
$current = $null
|
||||||
|
try {
|
||||||
|
$regResult = Get-GPRegistryValue -Name $GPOName -Key $regKey -ValueName $setting.ValueName `
|
||||||
|
-Domain $Domain -ErrorAction Stop
|
||||||
|
$current = $regResult.Value
|
||||||
|
} catch {
|
||||||
|
$current = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($current -ne $setting.Desired) {
|
||||||
|
$currentDisplay = if ($null -eq $current) { '(not set)' } else { $current }
|
||||||
|
Write-Host " [DRIFT] $profileName\$($setting.ValueName): $currentDisplay -> $($setting.Desired)" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'FirewallProfile'
|
||||||
|
Profile = $profileName
|
||||||
|
Setting = $setting.ValueName
|
||||||
|
Current = $currentDisplay
|
||||||
|
Desired = $setting.Desired
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [OK] $profileName\$($setting.ValueName) = $current" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($diffs.Count -eq 0) {
|
||||||
|
Write-Host " [OK] All firewall profiles match desired state" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-GPOFirewall {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Writes Windows Firewall rules to a GPO using Open-NetGPO session.
|
||||||
|
Full overwrite semantics -- removes all existing rules, then creates declared rules.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[array]$FirewallRules,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$domainFqdn = $Domain
|
||||||
|
Write-Host " Applying firewall rules..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Open a GPO session
|
||||||
|
$gpoSession = Open-NetGPO -PolicyStore "$domainFqdn\$GPOName"
|
||||||
|
|
||||||
|
# Remove all existing rules in this GPO
|
||||||
|
try {
|
||||||
|
$existing = Get-NetFirewallRule -GPOSession $gpoSession -ErrorAction SilentlyContinue
|
||||||
|
if ($existing) {
|
||||||
|
Remove-NetFirewallRule -GPOSession $gpoSession -All
|
||||||
|
Write-Host " Removed $(@($existing).Count) existing rule(s)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
# No existing rules -- nothing to remove
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create each declared rule
|
||||||
|
foreach ($rule in $FirewallRules) {
|
||||||
|
$params = @{
|
||||||
|
GPOSession = $gpoSession
|
||||||
|
DisplayName = $rule.DisplayName
|
||||||
|
Direction = $rule.Direction
|
||||||
|
Action = $rule.Action
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($rule.Protocol) { $params.Protocol = $rule.Protocol }
|
||||||
|
if ($rule.LocalPort) { $params.LocalPort = $rule.LocalPort }
|
||||||
|
if ($rule.RemotePort) { $params.RemotePort = $rule.RemotePort }
|
||||||
|
if ($rule.LocalAddress) { $params.LocalAddress = $rule.LocalAddress }
|
||||||
|
if ($rule.RemoteAddress) { $params.RemoteAddress = $rule.RemoteAddress }
|
||||||
|
if ($rule.Program) { $params.Program = $rule.Program }
|
||||||
|
if ($rule.Description) { $params.Description = $rule.Description }
|
||||||
|
|
||||||
|
if ($rule.ContainsKey('Enabled')) {
|
||||||
|
$params.Enabled = if ($rule.Enabled) { 'True' } else { 'False' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($rule.Profile) {
|
||||||
|
$params.Profile = $rule.Profile
|
||||||
|
}
|
||||||
|
|
||||||
|
New-NetFirewallRule @params | Out-Null
|
||||||
|
Write-Host " [CREATED] $($rule.DisplayName)" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save the GPO session
|
||||||
|
Save-NetGPO -GPOSession $gpoSession
|
||||||
|
Write-Host " [OK] $($FirewallRules.Count) firewall rule(s) applied" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-GPOFirewall {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compares desired firewall rules against current GPO state.
|
||||||
|
Reports missing, extra, and differing rules.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[array]$FirewallRules,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$domainFqdn = $Domain
|
||||||
|
$diffs = @()
|
||||||
|
|
||||||
|
Write-Host " Comparing firewall rules..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Read rules via PolicyStore (Get-NetFirewallRule has no -GPOSession parameter)
|
||||||
|
$policyStore = "$domainFqdn\$GPOName"
|
||||||
|
|
||||||
|
$existing = @()
|
||||||
|
try {
|
||||||
|
$existing = @(Get-NetFirewallRule -PolicyStore $policyStore -ErrorAction SilentlyContinue)
|
||||||
|
} catch {
|
||||||
|
# No rules found
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingByName = @{}
|
||||||
|
foreach ($r in $existing) {
|
||||||
|
$existingByName[$r.DisplayName] = $r
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for missing rules
|
||||||
|
foreach ($rule in $FirewallRules) {
|
||||||
|
if ($existingByName.ContainsKey($rule.DisplayName)) {
|
||||||
|
$current = $existingByName[$rule.DisplayName]
|
||||||
|
$mismatch = $false
|
||||||
|
|
||||||
|
if ($rule.Direction -and $current.Direction -ne $rule.Direction) { $mismatch = $true }
|
||||||
|
if ($rule.Action -and $current.Action -ne $rule.Action) { $mismatch = $true }
|
||||||
|
|
||||||
|
if ($mismatch) {
|
||||||
|
Write-Host " [DRIFT] Rule differs: $($rule.DisplayName)" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'FirewallRule'
|
||||||
|
Rule = $rule.DisplayName
|
||||||
|
Status = 'Differs'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [OK] $($rule.DisplayName)" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [DRIFT] Missing rule: $($rule.DisplayName)" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'FirewallRule'
|
||||||
|
Rule = $rule.DisplayName
|
||||||
|
Status = 'Missing'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for extra rules (in GPO but not declared)
|
||||||
|
$declaredNames = @{}
|
||||||
|
foreach ($rule in $FirewallRules) { $declaredNames[$rule.DisplayName] = $true }
|
||||||
|
|
||||||
|
foreach ($r in $existing) {
|
||||||
|
if (-not $declaredNames.ContainsKey($r.DisplayName)) {
|
||||||
|
Write-Host " [DRIFT] Extra rule: $($r.DisplayName)" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'FirewallRule'
|
||||||
|
Rule = $r.DisplayName
|
||||||
|
Status = 'Extra (undeclared)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($diffs.Count -eq 0) {
|
||||||
|
Write-Host " [OK] All firewall rules match desired state" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [DRIFT] $($diffs.Count) firewall rule difference(s) found" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
215
gpo/lib/GPOFolderRedirection.ps1
Normal file
215
gpo/lib/GPOFolderRedirection.ps1
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
# GPOFolderRedirection.ps1
|
||||||
|
# Folder Redirection management via GPO.
|
||||||
|
# Writes fdeploy1.ini to SYSVOL and registers the CSE extension GUIDs.
|
||||||
|
# Depends on: GPOCore.ps1 (Get-GPOSysvolPath, Add-GPOExtensionGuids)
|
||||||
|
|
||||||
|
$Script:FolderRedirectionCseGuid = '{25537BA6-77A8-11D2-9B6C-0000F8080861}'
|
||||||
|
$Script:FolderRedirectionToolGuid = '{88E729D6-BDC1-11D1-BD2A-00C04FB9603F}'
|
||||||
|
|
||||||
|
# Maps settings.ps1 key names to fdeploy1.ini section names
|
||||||
|
$Script:FolderSectionMap = @{
|
||||||
|
Desktop = '.Desktop'
|
||||||
|
Documents = '.Documents'
|
||||||
|
Pictures = '.Pictures'
|
||||||
|
Music = '.Music'
|
||||||
|
Videos = '.Videos'
|
||||||
|
Favorites = '.Favorites'
|
||||||
|
AppDataRoaming = '.AppData'
|
||||||
|
Contacts = '.Contacts'
|
||||||
|
Downloads = '.Downloads'
|
||||||
|
Links = '.Links'
|
||||||
|
Searches = '.Searches'
|
||||||
|
StartMenu = '.StartMenu'
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-FolderRedirectionIni {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Converts a FolderRedirection hashtable into fdeploy1.ini content.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$FolderRedirection
|
||||||
|
)
|
||||||
|
|
||||||
|
$sections = foreach ($folderName in $FolderRedirection.Keys) {
|
||||||
|
$folder = $FolderRedirection[$folderName]
|
||||||
|
$sectionName = $Script:FolderSectionMap[$folderName]
|
||||||
|
if (-not $sectionName) {
|
||||||
|
Write-Host " [WARN] Unknown folder: $folderName" -ForegroundColor Yellow
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$destPath = $folder.Path
|
||||||
|
$grantExclusive = if ($folder.GrantExclusive) { '0x00000001' } else { '0x00000000' }
|
||||||
|
$moveContents = if ($folder.MoveContents) { '0x00000001' } else { '0x00000000' }
|
||||||
|
|
||||||
|
# Status=0x0001 = basic redirection (follow this folder)
|
||||||
|
@"
|
||||||
|
[$sectionName]
|
||||||
|
Status=0x0001
|
||||||
|
DestPath=$destPath
|
||||||
|
GrantExclusive=$grantExclusive
|
||||||
|
MoveContents=$moveContents
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($sections -join "`r`n`r`n") + "`r`n"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-GPOFolderRedirection {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Writes fdeploy1.ini to SYSVOL for folder redirection.
|
||||||
|
Full overwrite semantics. User scope only.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$FolderRedirection,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host " Applying folder redirection..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$ini = ConvertTo-FolderRedirectionIni -FolderRedirection $FolderRedirection
|
||||||
|
|
||||||
|
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
|
||||||
|
$deployDir = Join-Path $sysvolPath 'User\Documents & Settings'
|
||||||
|
|
||||||
|
if (-not (Test-Path $deployDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $deployDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$iniPath = Join-Path $deployDir 'fdeploy1.ini'
|
||||||
|
|
||||||
|
# Write as UTF-16LE (same encoding as other INI files in SYSVOL)
|
||||||
|
$utf16le = [System.Text.UnicodeEncoding]::new($false, $true)
|
||||||
|
[System.IO.File]::WriteAllText($iniPath, $ini, $utf16le)
|
||||||
|
|
||||||
|
Write-Host " Written: User\Documents & Settings\fdeploy1.ini" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Register CSE extension GUIDs
|
||||||
|
Add-GPOExtensionGuids -GPOName $GPOName `
|
||||||
|
-CseGuid $Script:FolderRedirectionCseGuid `
|
||||||
|
-ToolGuid $Script:FolderRedirectionToolGuid `
|
||||||
|
-Scope User -Domain $Domain
|
||||||
|
|
||||||
|
# Report what was configured
|
||||||
|
foreach ($folderName in $FolderRedirection.Keys) {
|
||||||
|
Write-Host " $folderName -> $($FolderRedirection[$folderName].Path)" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-GPOFolderRedirection {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compares desired folder redirection against current fdeploy1.ini in SYSVOL.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$FolderRedirection,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$diffs = @()
|
||||||
|
|
||||||
|
Write-Host " Comparing folder redirection..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
|
||||||
|
$iniPath = Join-Path $sysvolPath 'User\Documents & Settings\fdeploy1.ini'
|
||||||
|
|
||||||
|
if (-not (Test-Path $iniPath)) {
|
||||||
|
Write-Host " [DRIFT] Missing: fdeploy1.ini" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'FolderRedirection'
|
||||||
|
Folder = '(all)'
|
||||||
|
Status = 'File missing'
|
||||||
|
}
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse existing INI
|
||||||
|
$currentContent = Get-Content $iniPath -Raw
|
||||||
|
$currentSections = @{}
|
||||||
|
$currentSection = $null
|
||||||
|
|
||||||
|
foreach ($line in ($currentContent -split "`r?`n")) {
|
||||||
|
$trimmed = $line.Trim()
|
||||||
|
if ($trimmed -match '^\[(.+)\]$') {
|
||||||
|
$currentSection = $Matches[1]
|
||||||
|
$currentSections[$currentSection] = @{}
|
||||||
|
} elseif ($currentSection -and $trimmed -match '^(.+?)=(.*)$') {
|
||||||
|
$currentSections[$currentSection][$Matches[1]] = $Matches[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($folderName in $FolderRedirection.Keys) {
|
||||||
|
$desired = $FolderRedirection[$folderName]
|
||||||
|
$sectionName = $Script:FolderSectionMap[$folderName]
|
||||||
|
if (-not $sectionName) { continue }
|
||||||
|
|
||||||
|
if (-not $currentSections.ContainsKey($sectionName)) {
|
||||||
|
Write-Host " [DRIFT] Missing section: $sectionName ($folderName)" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'FolderRedirection'
|
||||||
|
Folder = $folderName
|
||||||
|
Status = 'Missing section'
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$section = $currentSections[$sectionName]
|
||||||
|
|
||||||
|
# Check DestPath
|
||||||
|
$currentPath = $section['DestPath']
|
||||||
|
if ($currentPath -ne $desired.Path) {
|
||||||
|
$pathDisplay = if ($currentPath) { $currentPath } else { '(not set)' }
|
||||||
|
Write-Host " [DRIFT] $folderName DestPath: $pathDisplay -> $($desired.Path)" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'FolderRedirection'
|
||||||
|
Folder = $folderName
|
||||||
|
Status = "DestPath: $pathDisplay -> $($desired.Path)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [OK] $folderName -> $currentPath" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check GrantExclusive
|
||||||
|
$desiredGrant = if ($desired.GrantExclusive) { '0x00000001' } else { '0x00000000' }
|
||||||
|
if ($section['GrantExclusive'] -ne $desiredGrant) {
|
||||||
|
Write-Host " [DRIFT] $folderName GrantExclusive: $($section['GrantExclusive']) -> $desiredGrant" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'FolderRedirection'
|
||||||
|
Folder = $folderName
|
||||||
|
Status = "GrantExclusive mismatch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check MoveContents
|
||||||
|
$desiredMove = if ($desired.MoveContents) { '0x00000001' } else { '0x00000000' }
|
||||||
|
if ($section['MoveContents'] -ne $desiredMove) {
|
||||||
|
Write-Host " [DRIFT] $folderName MoveContents: $($section['MoveContents']) -> $desiredMove" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'FolderRedirection'
|
||||||
|
Folder = $folderName
|
||||||
|
Status = "MoveContents mismatch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($diffs.Count -eq 0) {
|
||||||
|
Write-Host " [OK] Folder redirection matches desired state" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [DRIFT] $($diffs.Count) folder redirection difference(s) found" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
22
gpo/lib/GPOHelper.ps1
Normal file
22
gpo/lib/GPOHelper.ps1
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# GPOHelper.ps1
|
||||||
|
# Loader: dot-sources the modular GPO helper library.
|
||||||
|
# All consumer scripts (Apply-GPOBaseline.ps1, Apply-DscBaseline.ps1, DSC configs)
|
||||||
|
# continue to load this single file -- zero breaking changes.
|
||||||
|
|
||||||
|
$libDir = $PSScriptRoot
|
||||||
|
|
||||||
|
# Core utilities must load first (other modules depend on them)
|
||||||
|
. (Join-Path $libDir 'GPOCore.ps1')
|
||||||
|
|
||||||
|
# Feature modules (order-independent)
|
||||||
|
. (Join-Path $libDir 'GPOPolicy.ps1')
|
||||||
|
. (Join-Path $libDir 'GPOPermissions.ps1')
|
||||||
|
. (Join-Path $libDir 'GPOScripts.ps1')
|
||||||
|
. (Join-Path $libDir 'GPOAudit.ps1')
|
||||||
|
. (Join-Path $libDir 'GPOPreferences.ps1')
|
||||||
|
. (Join-Path $libDir 'GPOWmiFilter.ps1')
|
||||||
|
. (Join-Path $libDir 'GPOBackup.ps1')
|
||||||
|
. (Join-Path $libDir 'GPOFirewall.ps1')
|
||||||
|
. (Join-Path $libDir 'GPOAppLocker.ps1')
|
||||||
|
. (Join-Path $libDir 'GPOWdac.ps1')
|
||||||
|
. (Join-Path $libDir 'GPOFolderRedirection.ps1')
|
||||||
351
gpo/lib/GPOPermissions.ps1
Normal file
351
gpo/lib/GPOPermissions.ps1
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
# GPOPermissions.ps1
|
||||||
|
# GPO links, management permissions, and security filtering.
|
||||||
|
# No dependencies on GPOCore.ps1 (uses only AD/GP cmdlets directly).
|
||||||
|
|
||||||
|
function Normalize-GPOLinkTo {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Normalizes the LinkTo value from settings.ps1 into a consistent
|
||||||
|
array of hashtables with Target, Order, and Enforced keys.
|
||||||
|
.DESCRIPTION
|
||||||
|
Supports three input formats:
|
||||||
|
- String: 'OU=...' (backward compatible, no order/enforcement management)
|
||||||
|
- Hashtable: @{ Target = 'OU=...'; Order = 1; Enforced = $true }
|
||||||
|
- Array: @( @{ Target = ... }, @{ Target = ... } )
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$LinkTo
|
||||||
|
)
|
||||||
|
|
||||||
|
# Single string
|
||||||
|
if ($LinkTo -is [string]) {
|
||||||
|
return @(@{ Target = $LinkTo; Order = $null; Enforced = $false })
|
||||||
|
}
|
||||||
|
|
||||||
|
# Single hashtable with Target key
|
||||||
|
if ($LinkTo -is [hashtable] -and $LinkTo.Target) {
|
||||||
|
return @(@{
|
||||||
|
Target = $LinkTo.Target
|
||||||
|
Order = if ($LinkTo.ContainsKey('Order')) { $LinkTo.Order } else { $null }
|
||||||
|
Enforced = if ($LinkTo.ContainsKey('Enforced')) { $LinkTo.Enforced } else { $false }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
# Array of hashtables or strings
|
||||||
|
if ($LinkTo -is [array]) {
|
||||||
|
$result = @()
|
||||||
|
foreach ($item in $LinkTo) {
|
||||||
|
if ($item -is [string]) {
|
||||||
|
$result += @{ Target = $item; Order = $null; Enforced = $false }
|
||||||
|
} elseif ($item -is [hashtable] -and $item.Target) {
|
||||||
|
$result += @{
|
||||||
|
Target = $item.Target
|
||||||
|
Order = if ($item.ContainsKey('Order')) { $item.Order } else { $null }
|
||||||
|
Enforced = if ($item.ContainsKey('Enforced')) { $item.Enforced } else { $false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-GPOLink {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Idempotently links a GPO to an OU. Optionally manages link order
|
||||||
|
and enforcement state.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$TargetOU,
|
||||||
|
|
||||||
|
$Order = $null,
|
||||||
|
|
||||||
|
[bool]$Enforced = $false,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$inheritance = Get-GPInheritance -Target $TargetOU
|
||||||
|
$linked = $inheritance.GpoLinks | Where-Object { $_.DisplayName -eq $GPOName }
|
||||||
|
|
||||||
|
if ($linked) {
|
||||||
|
$changed = $false
|
||||||
|
|
||||||
|
# Check enforcement
|
||||||
|
$currentEnforced = $linked.Enforced -eq 'Yes'
|
||||||
|
if ($currentEnforced -ne $Enforced) {
|
||||||
|
$enforcedStr = if ($Enforced) { 'Yes' } else { 'No' }
|
||||||
|
Set-GPLink -Name $GPOName -Target $TargetOU -Domain $Domain -Enforced $enforcedStr | Out-Null
|
||||||
|
Write-Host " [UPDATED] Enforced: $currentEnforced -> $Enforced on $TargetOU" -ForegroundColor Yellow
|
||||||
|
$changed = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check order (only if specified)
|
||||||
|
if ($null -ne $Order -and $linked.Order -ne $Order) {
|
||||||
|
Set-GPLink -Name $GPOName -Target $TargetOU -Domain $Domain -Order $Order | Out-Null
|
||||||
|
Write-Host " [UPDATED] Link order: $($linked.Order) -> $Order on $TargetOU" -ForegroundColor Yellow
|
||||||
|
$changed = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $changed) {
|
||||||
|
Write-Host " Already linked: $GPOName -> $TargetOU" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$params = @{
|
||||||
|
Name = $GPOName
|
||||||
|
Target = $TargetOU
|
||||||
|
Domain = $Domain
|
||||||
|
}
|
||||||
|
if ($null -ne $Order) { $params.Order = $Order }
|
||||||
|
if ($Enforced) { $params.Enforced = 'Yes' }
|
||||||
|
New-GPLink @params | Out-Null
|
||||||
|
Write-Host " Linked: $GPOName -> $TargetOU" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-GPOLink {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Checks whether a GPO is linked to the specified OU with correct
|
||||||
|
order and enforcement. Returns diff objects for any discrepancies.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$TargetOU,
|
||||||
|
|
||||||
|
$Order = $null,
|
||||||
|
|
||||||
|
[bool]$Enforced = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
$diffs = @()
|
||||||
|
$inheritance = Get-GPInheritance -Target $TargetOU
|
||||||
|
$linked = $inheritance.GpoLinks | Where-Object { $_.DisplayName -eq $GPOName }
|
||||||
|
|
||||||
|
if (-not $linked) {
|
||||||
|
Write-Host " [DRIFT] Not linked: $GPOName -> $TargetOU" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'GPOLink'
|
||||||
|
GPO = $GPOName
|
||||||
|
TargetOU = $TargetOU
|
||||||
|
Status = 'Not linked'
|
||||||
|
}
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host " [OK] Linked: $GPOName -> $TargetOU" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Check enforcement
|
||||||
|
$currentEnforced = $linked.Enforced -eq 'Yes'
|
||||||
|
if ($currentEnforced -ne $Enforced) {
|
||||||
|
Write-Host " [DRIFT] Enforced: $currentEnforced -> $Enforced on $TargetOU" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'GPOLinkEnforced'
|
||||||
|
GPO = $GPOName
|
||||||
|
TargetOU = $TargetOU
|
||||||
|
Current = $currentEnforced
|
||||||
|
Desired = $Enforced
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check order (only if specified)
|
||||||
|
if ($null -ne $Order -and $linked.Order -ne $Order) {
|
||||||
|
Write-Host " [DRIFT] Link order: $($linked.Order) -> $Order on $TargetOU" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'GPOLinkOrder'
|
||||||
|
GPO = $GPOName
|
||||||
|
TargetOU = $TargetOU
|
||||||
|
Current = $linked.Order
|
||||||
|
Desired = $Order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Security Filtering (Deny Apply Group Policy)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
|
||||||
|
function Ensure-GPOSecurityFiltering {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Adds Deny Apply Group Policy ACEs to a GPO for specified groups.
|
||||||
|
Uses the AD ACL on the GPO object -- Set-GPPermission doesn't support Deny.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[string[]]$DenyApply = @()
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($DenyApply.Count -eq 0) { return }
|
||||||
|
|
||||||
|
$gpo = Get-GPO -Name $GPOName -ErrorAction Stop
|
||||||
|
$domainDN = (Get-ADDomain).DistinguishedName
|
||||||
|
$gpoADPath = "AD:CN={$($gpo.Id)},CN=Policies,CN=System,$domainDN"
|
||||||
|
$acl = Get-Acl $gpoADPath
|
||||||
|
|
||||||
|
# Apply Group Policy extended right GUID
|
||||||
|
$applyGpoGuid = [Guid]'edacfd8f-ffb3-11d1-b41d-00a0c968f939'
|
||||||
|
|
||||||
|
foreach ($groupName in $DenyApply) {
|
||||||
|
$group = Get-ADGroup -Identity $groupName -ErrorAction Stop
|
||||||
|
$sid = New-Object System.Security.Principal.SecurityIdentifier($group.SID)
|
||||||
|
|
||||||
|
# Check if a Deny rule already exists for this SID + extended right
|
||||||
|
$alreadyDenied = $acl.Access | Where-Object {
|
||||||
|
$_.AccessControlType -eq 'Deny' -and
|
||||||
|
$_.ObjectType -eq $applyGpoGuid -and
|
||||||
|
$_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -eq $sid.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($alreadyDenied) {
|
||||||
|
Write-Host " [OK] $groupName already denied Apply on GPO: $GPOName" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
$denyRule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
|
||||||
|
$sid,
|
||||||
|
[System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight,
|
||||||
|
[System.Security.AccessControl.AccessControlType]::Deny,
|
||||||
|
$applyGpoGuid
|
||||||
|
)
|
||||||
|
$acl.AddAccessRule($denyRule)
|
||||||
|
Set-Acl $gpoADPath $acl
|
||||||
|
Write-Host " [DENY] $groupName denied Apply on GPO: $GPOName" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-GPOSecurityFiltering {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Checks whether Deny Apply Group Policy ACEs exist for the specified groups.
|
||||||
|
Returns diffs for any missing deny rules.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[string[]]$DenyApply = @()
|
||||||
|
)
|
||||||
|
|
||||||
|
$diffs = @()
|
||||||
|
if ($DenyApply.Count -eq 0) { return $diffs }
|
||||||
|
|
||||||
|
$gpo = Get-GPO -Name $GPOName -ErrorAction Stop
|
||||||
|
$domainDN = (Get-ADDomain).DistinguishedName
|
||||||
|
$gpoADPath = "AD:CN={$($gpo.Id)},CN=Policies,CN=System,$domainDN"
|
||||||
|
$acl = Get-Acl $gpoADPath
|
||||||
|
|
||||||
|
$applyGpoGuid = [Guid]'edacfd8f-ffb3-11d1-b41d-00a0c968f939'
|
||||||
|
|
||||||
|
foreach ($groupName in $DenyApply) {
|
||||||
|
$group = Get-ADGroup -Identity $groupName -ErrorAction Stop
|
||||||
|
$sid = New-Object System.Security.Principal.SecurityIdentifier($group.SID)
|
||||||
|
|
||||||
|
$found = $acl.Access | Where-Object {
|
||||||
|
$_.AccessControlType -eq 'Deny' -and
|
||||||
|
$_.ObjectType -eq $applyGpoGuid -and
|
||||||
|
$_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -eq $sid.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($found) {
|
||||||
|
Write-Host " [OK] $groupName denied Apply on GPO: $GPOName" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [DRIFT] $groupName not denied Apply on GPO: $GPOName" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'SecurityFiltering'
|
||||||
|
GPO = $GPOName
|
||||||
|
Group = $groupName
|
||||||
|
Status = 'Missing Deny'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# GPO Management Permissions
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
|
||||||
|
function Ensure-GPOManagementPermission {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Ensures a security group has GpoEditDeleteModifySecurity on a GPO.
|
||||||
|
Idempotent: no-op if the permission already exists.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GroupName
|
||||||
|
)
|
||||||
|
|
||||||
|
$hasPermission = $false
|
||||||
|
try {
|
||||||
|
$perm = Get-GPPermission -Name $GPOName -TargetName $GroupName -TargetType Group -ErrorAction Stop
|
||||||
|
if ($perm.Permission -eq 'GpoEditDeleteModifySecurity') {
|
||||||
|
$hasPermission = $true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
# Group has no permissions on this GPO
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasPermission) {
|
||||||
|
Write-Host " [OK] $GroupName has edit rights on GPO: $GPOName" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Set-GPPermission -Name $GPOName -PermissionLevel GpoEditDeleteModifySecurity `
|
||||||
|
-TargetName $GroupName -TargetType Group -Replace | Out-Null
|
||||||
|
Write-Host " [GRANTED] $GroupName edit rights on GPO: $GPOName" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-GPOManagementPermission {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Checks whether a security group has GpoEditDeleteModifySecurity on a GPO.
|
||||||
|
Returns a diff object if the permission is missing.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GroupName
|
||||||
|
)
|
||||||
|
|
||||||
|
$hasPermission = $false
|
||||||
|
try {
|
||||||
|
$perm = Get-GPPermission -Name $GPOName -TargetName $GroupName -TargetType Group -ErrorAction Stop
|
||||||
|
if ($perm.Permission -eq 'GpoEditDeleteModifySecurity') {
|
||||||
|
$hasPermission = $true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
# Group has no permissions on this GPO
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasPermission) {
|
||||||
|
Write-Host " [OK] $GroupName has edit rights on GPO: $GPOName" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [DRIFT] $GroupName missing edit rights on GPO: $GPOName" -ForegroundColor Red
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Type = 'ManagementPermission'
|
||||||
|
GPO = $GPOName
|
||||||
|
Group = $GroupName
|
||||||
|
Status = 'Missing GpoEditDeleteModifySecurity'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
377
gpo/lib/GPOPolicy.ps1
Normal file
377
gpo/lib/GPOPolicy.ps1
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
# GPOPolicy.ps1
|
||||||
|
# GptTmpl.inf handling and registry-based Administrative Template settings.
|
||||||
|
# Depends on: GPOCore.ps1 (Get-GPOSysvolPath, Get-GPOSecurityTemplatePath)
|
||||||
|
|
||||||
|
function Read-GptTmplInf {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Parses a GptTmpl.inf file into a nested hashtable keyed by section name.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Path
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-Path $Path)) {
|
||||||
|
return @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = @{}
|
||||||
|
$currentSection = $null
|
||||||
|
|
||||||
|
# GptTmpl.inf is UTF-16LE
|
||||||
|
$lines = [System.IO.File]::ReadAllLines($Path, [System.Text.Encoding]::Unicode)
|
||||||
|
|
||||||
|
foreach ($line in $lines) {
|
||||||
|
$line = $line.Trim()
|
||||||
|
if ($line -match '^\[(.+)\]$') {
|
||||||
|
$currentSection = $Matches[1]
|
||||||
|
if (-not $result.Contains($currentSection)) {
|
||||||
|
$result[$currentSection] = [ordered]@{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($line -match '^(.+?)\s*=\s*(.*)$' -and $currentSection) {
|
||||||
|
$result[$currentSection][$Matches[1].Trim()] = $Matches[2].Trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-GptTmplInf {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Converts a settings hashtable into GptTmpl.inf content string.
|
||||||
|
.DESCRIPTION
|
||||||
|
Takes the SecurityPolicy hashtable from a settings.ps1 file and produces
|
||||||
|
the INI-format content for a GptTmpl.inf file.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
$sb = [System.Text.StringBuilder]::new()
|
||||||
|
[void]$sb.AppendLine('[Unicode]')
|
||||||
|
[void]$sb.AppendLine('Unicode=yes')
|
||||||
|
|
||||||
|
# Ordered section output
|
||||||
|
$sectionOrder = @(
|
||||||
|
'System Access'
|
||||||
|
'Kerberos Policy'
|
||||||
|
'Event Audit'
|
||||||
|
'Registry Values'
|
||||||
|
'Privilege Rights'
|
||||||
|
'Group Membership'
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($section in $sectionOrder) {
|
||||||
|
if ($Settings.Contains($section)) {
|
||||||
|
[void]$sb.AppendLine("[$section]")
|
||||||
|
foreach ($key in $Settings[$section].Keys) {
|
||||||
|
[void]$sb.AppendLine("$key = $($Settings[$section][$key])")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Include any sections not in the predefined order
|
||||||
|
foreach ($section in $Settings.Keys) {
|
||||||
|
if ($section -notin $sectionOrder -and $section -ne 'Unicode') {
|
||||||
|
[void]$sb.AppendLine("[$section]")
|
||||||
|
foreach ($key in $Settings[$section].Keys) {
|
||||||
|
[void]$sb.AppendLine("$key = $($Settings[$section][$key])")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[void]$sb.AppendLine('[Version]')
|
||||||
|
[void]$sb.AppendLine('signature="$CHICAGO$"')
|
||||||
|
[void]$sb.AppendLine('Revision=1')
|
||||||
|
|
||||||
|
return $sb.ToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-GPOSecurityPolicy {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Writes security policy settings to a GPO's GptTmpl.inf in SYSVOL
|
||||||
|
and bumps the GPO version number in AD so clients pick up the change.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$SecurityPolicy,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$infPath = Get-GPOSecurityTemplatePath -GPOName $GPOName -Domain $Domain
|
||||||
|
$infDir = Split-Path $infPath -Parent
|
||||||
|
|
||||||
|
# Ensure directory structure exists
|
||||||
|
if (-not (Test-Path $infDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $infDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate and write the inf content as UTF-16LE (required by Windows)
|
||||||
|
$content = ConvertTo-GptTmplInf -Settings $SecurityPolicy
|
||||||
|
[System.IO.File]::WriteAllText($infPath, $content, [System.Text.Encoding]::Unicode)
|
||||||
|
|
||||||
|
Write-Host " Written: $infPath" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-GPOSecurityPolicy {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compares desired security policy settings against what's currently
|
||||||
|
in the GPO's GptTmpl.inf. Returns differences.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$SecurityPolicy,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$infPath = Get-GPOSecurityTemplatePath -GPOName $GPOName -Domain $Domain
|
||||||
|
$current = Read-GptTmplInf -Path $infPath
|
||||||
|
$differences = @()
|
||||||
|
|
||||||
|
foreach ($section in $SecurityPolicy.Keys) {
|
||||||
|
foreach ($key in $SecurityPolicy[$section].Keys) {
|
||||||
|
$desired = [string]$SecurityPolicy[$section][$key]
|
||||||
|
$actual = $null
|
||||||
|
|
||||||
|
if ($current.Contains($section) -and $current[$section].Contains($key)) {
|
||||||
|
$actual = [string]$current[$section][$key]
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($actual -ne $desired) {
|
||||||
|
$differences += [PSCustomObject]@{
|
||||||
|
Section = $section
|
||||||
|
Setting = $key
|
||||||
|
Current = if ($null -eq $actual) { '(not set)' } else { $actual }
|
||||||
|
Desired = $desired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $differences
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Restricted Groups ([Group Membership] section in GptTmpl.inf)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
|
||||||
|
function ConvertTo-RestrictedGroupEntries {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Converts a friendly RestrictedGroups hashtable into [Group Membership]
|
||||||
|
key-value pairs for GptTmpl.inf.
|
||||||
|
.DESCRIPTION
|
||||||
|
GptTmpl.inf [Group Membership] uses SID-based keys:
|
||||||
|
*S-1-5-32-544__Members = *S-1-5-21-xxx,*S-1-5-21-yyy
|
||||||
|
*S-1-5-32-544__Memberof =
|
||||||
|
This function resolves group/account names to SIDs automatically.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$RestrictedGroups
|
||||||
|
)
|
||||||
|
|
||||||
|
$entries = [ordered]@{}
|
||||||
|
|
||||||
|
foreach ($groupName in $RestrictedGroups.Keys) {
|
||||||
|
$groupDef = $RestrictedGroups[$groupName]
|
||||||
|
|
||||||
|
# Resolve target group to SID
|
||||||
|
try {
|
||||||
|
$ntAccount = New-Object System.Security.Principal.NTAccount($groupName)
|
||||||
|
$groupSid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
||||||
|
} catch {
|
||||||
|
throw "RestrictedGroups: cannot resolve group '$groupName' to a SID: $_"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Members
|
||||||
|
$memberSids = @()
|
||||||
|
if ($groupDef.Members) {
|
||||||
|
foreach ($member in $groupDef.Members) {
|
||||||
|
try {
|
||||||
|
$memberNt = New-Object System.Security.Principal.NTAccount($member)
|
||||||
|
$memberSid = $memberNt.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
||||||
|
$memberSids += "*$memberSid"
|
||||||
|
} catch {
|
||||||
|
throw "RestrictedGroups: cannot resolve member '$member' of group '$groupName' to a SID: $_"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$entries["*${groupSid}__Members"] = $memberSids -join ','
|
||||||
|
|
||||||
|
# Memberof
|
||||||
|
$memberofSids = @()
|
||||||
|
if ($groupDef.Memberof) {
|
||||||
|
foreach ($parent in $groupDef.Memberof) {
|
||||||
|
try {
|
||||||
|
$parentNt = New-Object System.Security.Principal.NTAccount($parent)
|
||||||
|
$parentSid = $parentNt.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
||||||
|
$memberofSids += "*$parentSid"
|
||||||
|
} catch {
|
||||||
|
throw "RestrictedGroups: cannot resolve parent group '$parent' of '$groupName' to a SID: $_"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$entries["*${groupSid}__Memberof"] = $memberofSids -join ','
|
||||||
|
}
|
||||||
|
|
||||||
|
return $entries
|
||||||
|
}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
# Registry-Based Settings (Administrative Templates)
|
||||||
|
# -------------------------------------------------------------------
|
||||||
|
|
||||||
|
function Compare-GPORegistrySettings {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compares desired registry-based (Administrative Template) settings
|
||||||
|
against the current values in a GPO. Returns differences.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[array]$RegistrySettings,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$differences = @()
|
||||||
|
|
||||||
|
foreach ($reg in $RegistrySettings) {
|
||||||
|
$desiredDisplay = "$($reg.Value) ($($reg.Type))"
|
||||||
|
|
||||||
|
try {
|
||||||
|
$current = Get-GPRegistryValue `
|
||||||
|
-Name $GPOName `
|
||||||
|
-Domain $Domain `
|
||||||
|
-Key $reg.Key `
|
||||||
|
-ValueName $reg.ValueName `
|
||||||
|
-ErrorAction Stop
|
||||||
|
|
||||||
|
$actualValue = $current.Value
|
||||||
|
$actualType = $current.Type.ToString()
|
||||||
|
|
||||||
|
if ([string]$actualValue -ne [string]$reg.Value -or $actualType -ne $reg.Type) {
|
||||||
|
$differences += [PSCustomObject]@{
|
||||||
|
Key = $reg.Key
|
||||||
|
ValueName = $reg.ValueName
|
||||||
|
Current = "$actualValue ($actualType)"
|
||||||
|
Desired = $desiredDisplay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
# Setting doesn't exist in the GPO yet
|
||||||
|
$differences += [PSCustomObject]@{
|
||||||
|
Key = $reg.Key
|
||||||
|
ValueName = $reg.ValueName
|
||||||
|
Current = '(not set)'
|
||||||
|
Desired = $desiredDisplay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Stale value detection ---
|
||||||
|
# For each unique key in settings, check if the GPO has values not in the declared set
|
||||||
|
$uniqueKeys = $RegistrySettings | ForEach-Object { $_.Key } | Select-Object -Unique
|
||||||
|
$declaredLookup = @{}
|
||||||
|
foreach ($reg in $RegistrySettings) {
|
||||||
|
$declaredLookup["$($reg.Key)|$($reg.ValueName)"] = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($key in $uniqueKeys) {
|
||||||
|
try {
|
||||||
|
$currentValues = Get-GPRegistryValue -Name $GPOName -Domain $Domain `
|
||||||
|
-Key $key -ErrorAction Stop
|
||||||
|
} catch { continue }
|
||||||
|
|
||||||
|
foreach ($val in $currentValues) {
|
||||||
|
if (-not $val.ValueName) { continue } # skip subkey entries
|
||||||
|
$lookupKey = "$key|$($val.ValueName)"
|
||||||
|
if (-not $declaredLookup.ContainsKey($lookupKey)) {
|
||||||
|
$differences += [PSCustomObject]@{
|
||||||
|
Key = $key
|
||||||
|
ValueName = $val.ValueName
|
||||||
|
Current = "$($val.Value) ($($val.Type))"
|
||||||
|
Desired = '(stale -- should be removed)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $differences
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-GPORegistrySettings {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Applies registry-based (Administrative Template) settings to a GPO
|
||||||
|
using Set-GPRegistryValue. With -Cleanup, removes stale values under
|
||||||
|
managed keys that are not in the declared settings.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[array]$RegistrySettings,
|
||||||
|
|
||||||
|
[switch]$Cleanup,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($reg in $RegistrySettings) {
|
||||||
|
Set-GPRegistryValue `
|
||||||
|
-Name $GPOName `
|
||||||
|
-Domain $Domain `
|
||||||
|
-Key $reg.Key `
|
||||||
|
-ValueName $reg.ValueName `
|
||||||
|
-Type $reg.Type `
|
||||||
|
-Value $reg.Value | Out-Null
|
||||||
|
|
||||||
|
Write-Host " Set: $($reg.Key)\$($reg.ValueName) = $($reg.Value)" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove stale values under managed keys
|
||||||
|
if ($Cleanup) {
|
||||||
|
$declaredLookup = @{}
|
||||||
|
foreach ($reg in $RegistrySettings) {
|
||||||
|
$declaredLookup["$($reg.Key)|$($reg.ValueName)"] = $true
|
||||||
|
}
|
||||||
|
$uniqueKeys = $RegistrySettings | ForEach-Object { $_.Key } | Select-Object -Unique
|
||||||
|
|
||||||
|
foreach ($key in $uniqueKeys) {
|
||||||
|
try {
|
||||||
|
$currentValues = Get-GPRegistryValue -Name $GPOName -Domain $Domain `
|
||||||
|
-Key $key -ErrorAction Stop
|
||||||
|
} catch { continue }
|
||||||
|
|
||||||
|
foreach ($val in $currentValues) {
|
||||||
|
if (-not $val.ValueName) { continue }
|
||||||
|
$lookupKey = "$key|$($val.ValueName)"
|
||||||
|
if (-not $declaredLookup.ContainsKey($lookupKey)) {
|
||||||
|
Remove-GPRegistryValue -Name $GPOName -Domain $Domain `
|
||||||
|
-Key $key -ValueName $val.ValueName | Out-Null
|
||||||
|
Write-Host " [REMOVED] Stale: $key\$($val.ValueName)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
837
gpo/lib/GPOPreferences.ps1
Normal file
837
gpo/lib/GPOPreferences.ps1
Normal file
@ -0,0 +1,837 @@
|
|||||||
|
# GPOPreferences.ps1
|
||||||
|
# Group Policy Preferences XML generation for 10 types.
|
||||||
|
# Depends on: GPOCore.ps1 (Get-GPOSysvolPath, Add-GPOExtensionGuids)
|
||||||
|
|
||||||
|
$Script:GppToolGuid = '{CAB54552-DEEA-4691-817E-ED4A4D1AFC72}'
|
||||||
|
|
||||||
|
$Script:GppActionCode = @{ Create = 'C'; Replace = 'R'; Update = 'U'; Delete = 'D' }
|
||||||
|
$Script:GppActionImage = @{ Create = '0'; Replace = '1'; Update = '2'; Delete = '3' }
|
||||||
|
|
||||||
|
$Script:GppTypeInfo = @{
|
||||||
|
ScheduledTasks = @{
|
||||||
|
RootClsid = '{CC63F200-7309-4ba0-B154-A71CD118DBCC}'
|
||||||
|
ItemClsid = '{D8896631-B747-47a7-84A6-C155337F3BC8}'
|
||||||
|
SysvolDir = 'ScheduledTasks'
|
||||||
|
FileName = 'ScheduledTasks.xml'
|
||||||
|
CseGuid = '{AADCED64-746C-4633-A97C-D61349046527}'
|
||||||
|
NameKey = 'Name'
|
||||||
|
}
|
||||||
|
DriveMaps = @{
|
||||||
|
RootClsid = '{8FDDCC1A-0C3C-43cd-A6B4-71A6DF20DA8C}'
|
||||||
|
ItemClsid = '{935D1B74-9CB8-4e3c-9914-7DD559B7A417}'
|
||||||
|
SysvolDir = 'Drives'
|
||||||
|
FileName = 'Drives.xml'
|
||||||
|
CseGuid = '{5794DAFD-BE60-433f-88A2-1A31939AC01F}'
|
||||||
|
NameKey = 'Letter'
|
||||||
|
}
|
||||||
|
EnvironmentVariables = @{
|
||||||
|
RootClsid = '{D76B9641-3288-4f75-942D-087DE603E3EA}'
|
||||||
|
ItemClsid = '{B1A3FA3F-E5D1-41ea-88D1-55CE96B6C78B}'
|
||||||
|
SysvolDir = 'EnvironmentVariables'
|
||||||
|
FileName = 'EnvironmentVariables.xml'
|
||||||
|
CseGuid = '{0E28E245-9368-4853-AD84-6DA3BA35BB75}'
|
||||||
|
NameKey = 'Name'
|
||||||
|
}
|
||||||
|
Services = @{
|
||||||
|
RootClsid = '{2CFB484A-4E86-4eb1-8B6A-E1535488BDBF}'
|
||||||
|
ItemClsid = '{AB6F0B67-D4E1-4f59-A9E5-F6BD3A72F4FF}'
|
||||||
|
SysvolDir = 'Services'
|
||||||
|
FileName = 'Services.xml'
|
||||||
|
CseGuid = '{91FBB303-0CD5-4055-BF42-E512A681B325}'
|
||||||
|
NameKey = 'ServiceName'
|
||||||
|
}
|
||||||
|
Printers = @{
|
||||||
|
RootClsid = '{1F577D12-3D1B-471e-A1B7-060317597B9C}'
|
||||||
|
ItemClsid = '{9A5E9697-9095-436d-A0EE-4D128FDFBCE5}'
|
||||||
|
SysvolDir = 'Printers'
|
||||||
|
FileName = 'Printers.xml'
|
||||||
|
CseGuid = '{A8C42CEA-CDB8-4388-97F4-5831F933DA84}'
|
||||||
|
NameKey = 'Path'
|
||||||
|
}
|
||||||
|
Shortcuts = @{
|
||||||
|
RootClsid = '{872ECB34-B2EC-401b-A585-D32574AA90EE}'
|
||||||
|
ItemClsid = '{4F2F7C55-2790-433e-8127-0739D1CFA327}'
|
||||||
|
SysvolDir = 'Shortcuts'
|
||||||
|
FileName = 'Shortcuts.xml'
|
||||||
|
CseGuid = '{C418DD9D-0D14-4EFB-8FBF-CFE535C8FAC7}'
|
||||||
|
NameKey = 'Name'
|
||||||
|
}
|
||||||
|
Files = @{
|
||||||
|
RootClsid = '{215B2E53-57CE-475c-80FE-9EEC14635851}'
|
||||||
|
ItemClsid = '{50BE44C8-567A-4ed1-B1D0-9234FE1F38AF}'
|
||||||
|
SysvolDir = 'Files'
|
||||||
|
FileName = 'Files.xml'
|
||||||
|
CseGuid = '{7150F9BF-48AD-4DA4-A49C-29EF4A8369BA}'
|
||||||
|
NameKey = 'TargetPath'
|
||||||
|
}
|
||||||
|
NetworkShares = @{
|
||||||
|
RootClsid = '{520870D8-A6E7-47e8-A8D8-E6A4E76EAEC2}'
|
||||||
|
ItemClsid = '{2888C5E7-94FC-4739-90AA-2C1536D68BC0}'
|
||||||
|
SysvolDir = 'NetworkShares'
|
||||||
|
FileName = 'NetworkShares.xml'
|
||||||
|
CseGuid = '{6A4C88C6-C502-4F74-8F60-2CB23EDC24E2}'
|
||||||
|
NameKey = 'Name'
|
||||||
|
}
|
||||||
|
RegistryItems = @{
|
||||||
|
RootClsid = '{A3CCFC41-DFDB-43a5-8D26-0FE8B954DA51}'
|
||||||
|
ItemClsid = '{9CD4B2F4-923D-47f5-A062-E897DD1DAD50}'
|
||||||
|
SysvolDir = 'Registry'
|
||||||
|
FileName = 'Registry.xml'
|
||||||
|
CseGuid = '{B087BE9D-ED37-454F-AF9C-04291E351182}'
|
||||||
|
NameKey = 'Name'
|
||||||
|
}
|
||||||
|
LocalUsersAndGroups = @{
|
||||||
|
RootClsid = '{3125E937-EB16-4b4c-9934-544FC6D24D26}'
|
||||||
|
ItemClsid = '{6D4A79E4-529C-4481-ABD0-F5BD7EA93BA7}'
|
||||||
|
SysvolDir = 'Groups'
|
||||||
|
FileName = 'Groups.xml'
|
||||||
|
CseGuid = '{17D89FEC-5C44-4972-B12D-241CAEF74509}'
|
||||||
|
NameKey = 'GroupName'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-ILTFilterXml {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Converts a Filters array from a GPP item into the ILT <Filters> XML block.
|
||||||
|
Returns empty string if no filters are defined.
|
||||||
|
.DESCRIPTION
|
||||||
|
Supports SecurityGroup, OrgUnit, Computer, User, OperatingSystem, and WMI
|
||||||
|
filter types. SecurityGroup filters resolve names to SIDs at runtime.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[array]$Filters
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $Filters -or $Filters.Count -eq 0) { return '' }
|
||||||
|
|
||||||
|
$esc = [System.Security.SecurityElement]
|
||||||
|
|
||||||
|
$filterXml = foreach ($f in $Filters) {
|
||||||
|
$bool = if ($f.Bool) { $f.Bool } else { 'AND' }
|
||||||
|
$not = if ($f.Not) { '1' } else { '0' }
|
||||||
|
$name = $esc::Escape($f.Name)
|
||||||
|
|
||||||
|
switch ($f.Type) {
|
||||||
|
'SecurityGroup' {
|
||||||
|
try {
|
||||||
|
$ntAccount = New-Object System.Security.Principal.NTAccount($f.Name)
|
||||||
|
$sid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
||||||
|
} catch {
|
||||||
|
throw "ILT Filter: cannot resolve group '$($f.Name)' to a SID: $_"
|
||||||
|
}
|
||||||
|
$userContext = if ($f.ContainsKey('UserContext') -and -not $f.UserContext) { '0' } else { '1' }
|
||||||
|
$primaryGroup = if ($f.PrimaryGroup) { '1' } else { '0' }
|
||||||
|
$localGroup = if ($f.LocalGroup) { '1' } else { '0' }
|
||||||
|
" <FilterGroup bool=`"$bool`" not=`"$not`" name=`"$name`" sid=`"$sid`" userContext=`"$userContext`" primaryGroup=`"$primaryGroup`" localGroup=`"$localGroup`"/>"
|
||||||
|
}
|
||||||
|
'OrgUnit' {
|
||||||
|
$type = if ($f.ContainsKey('OUType')) { $f.OUType } else { '' }
|
||||||
|
" <FilterOrgUnit bool=`"$bool`" not=`"$not`" name=`"$name`" type=`"$type`"/>"
|
||||||
|
}
|
||||||
|
'Computer' {
|
||||||
|
$type = if ($f.ContainsKey('NameType')) { $f.NameType } else { 'NETBIOS' }
|
||||||
|
" <FilterComputer bool=`"$bool`" not=`"$not`" type=`"$type`" name=`"$name`"/>"
|
||||||
|
}
|
||||||
|
'User' {
|
||||||
|
$type = if ($f.ContainsKey('NameType')) { $f.NameType } else { 'NETBIOS' }
|
||||||
|
" <FilterUser bool=`"$bool`" not=`"$not`" type=`"$type`" name=`"$name`"/>"
|
||||||
|
}
|
||||||
|
'OperatingSystem' {
|
||||||
|
$edition = if ($f.Edition) { $esc::Escape($f.Edition) } else { '' }
|
||||||
|
$version = if ($f.Version) { $esc::Escape($f.Version) } else { '' }
|
||||||
|
" <FilterOperatingSystem bool=`"$bool`" not=`"$not`" type=`"$name`" edition=`"$edition`" version=`"$version`"/>"
|
||||||
|
}
|
||||||
|
'WMI' {
|
||||||
|
$property = if ($f.Property) { $esc::Escape($f.Property) } else { '' }
|
||||||
|
$namespace = if ($f.Namespace) { $esc::Escape($f.Namespace) } else { 'root\CIMv2' }
|
||||||
|
$query = $esc::Escape($f.Query)
|
||||||
|
" <FilterWmi bool=`"$bool`" not=`"$not`" name=`"$name`" property=`"$property`" query=`"$query`" namespace=`"$namespace`"/>"
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
Write-Host " [WARN] Unknown ILT filter type: $($f.Type)" -ForegroundColor Yellow
|
||||||
|
$null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$validFilters = @($filterXml | Where-Object { $_ })
|
||||||
|
if ($validFilters.Count -eq 0) { return '' }
|
||||||
|
|
||||||
|
return "`n <Filters>`n$($validFilters -join "`n")`n </Filters>"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-ScheduledTaskXml {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Generates ScheduledTasks.xml GPP content for TaskV2 entries.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[array]$Tasks,
|
||||||
|
|
||||||
|
[string]$Scope = 'Machine'
|
||||||
|
)
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
$esc = [System.Security.SecurityElement]
|
||||||
|
$userContext = if ($Scope -eq 'User') { '1' } else { '0' }
|
||||||
|
|
||||||
|
$itemsXml = foreach ($task in $Tasks) {
|
||||||
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
||||||
|
$action = $Script:GppActionCode[$task.Action]
|
||||||
|
$image = $Script:GppActionImage[$task.Action]
|
||||||
|
$name = $esc::Escape($task.Name)
|
||||||
|
$runAs = if ($task.RunAs) { $task.RunAs } else { 'NT AUTHORITY\System' }
|
||||||
|
$command = $esc::Escape($task.Command)
|
||||||
|
$arguments = if ($task.Arguments) { $esc::Escape($task.Arguments) } else { '' }
|
||||||
|
|
||||||
|
$trigger = switch ($task.Trigger) {
|
||||||
|
'AtStartup' { '<BootTrigger><Enabled>true</Enabled></BootTrigger>' }
|
||||||
|
'AtLogon' { '<LogonTrigger><Enabled>true</Enabled></LogonTrigger>' }
|
||||||
|
default { '<BootTrigger><Enabled>true</Enabled></BootTrigger>' }
|
||||||
|
}
|
||||||
|
|
||||||
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $task.Filters
|
||||||
|
|
||||||
|
@"
|
||||||
|
<TaskV2 clsid="{D8896631-B747-47a7-84A6-C155337F3BC8}" name="$name" image="$image" changed="$timestamp" uid="$uid" userContext="$userContext" removePolicy="0">
|
||||||
|
<Properties action="$action" name="$name" runAs="$runAs" logonType="S4U">
|
||||||
|
<Task version="1.2">
|
||||||
|
<RegistrationInfo><Description></Description></RegistrationInfo>
|
||||||
|
<Principals>
|
||||||
|
<Principal id="Author">
|
||||||
|
<UserId>$runAs</UserId>
|
||||||
|
<LogonType>S4U</LogonType>
|
||||||
|
<RunLevel>HighestAvailable</RunLevel>
|
||||||
|
</Principal>
|
||||||
|
</Principals>
|
||||||
|
<Settings>
|
||||||
|
<IdleSettings>
|
||||||
|
<Duration>PT10M</Duration>
|
||||||
|
<WaitTimeout>PT1H</WaitTimeout>
|
||||||
|
<StopOnIdleEnd>true</StopOnIdleEnd>
|
||||||
|
<RestartOnIdle>false</RestartOnIdle>
|
||||||
|
</IdleSettings>
|
||||||
|
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
||||||
|
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||||
|
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||||
|
<AllowHardTerminate>true</AllowHardTerminate>
|
||||||
|
<StartWhenAvailable>true</StartWhenAvailable>
|
||||||
|
<AllowStartOnDemand>true</AllowStartOnDemand>
|
||||||
|
<Enabled>true</Enabled>
|
||||||
|
<Hidden>false</Hidden>
|
||||||
|
<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
|
||||||
|
<Priority>7</Priority>
|
||||||
|
</Settings>
|
||||||
|
<Triggers>$trigger</Triggers>
|
||||||
|
<Actions>
|
||||||
|
<Exec>
|
||||||
|
<Command>$command</Command>
|
||||||
|
<Arguments>$arguments</Arguments>
|
||||||
|
</Exec>
|
||||||
|
</Actions>
|
||||||
|
</Task>
|
||||||
|
</Properties>$filterBlock
|
||||||
|
</TaskV2>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A71CD118DBCC}">
|
||||||
|
$($itemsXml -join "`n")
|
||||||
|
</ScheduledTasks>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-DriveMapXml {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Generates Drives.xml GPP content for drive map entries.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[array]$Drives
|
||||||
|
)
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
$esc = [System.Security.SecurityElement]
|
||||||
|
|
||||||
|
$itemsXml = foreach ($drive in $Drives) {
|
||||||
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
||||||
|
$action = $Script:GppActionCode[$drive.Action]
|
||||||
|
$image = $Script:GppActionImage[$drive.Action]
|
||||||
|
$letter = $drive.Letter.TrimEnd(':')
|
||||||
|
$path = $esc::Escape($drive.Path)
|
||||||
|
$label = if ($drive.Label) { $esc::Escape($drive.Label) } else { '' }
|
||||||
|
$persistent = if ($drive.Reconnect) { '1' } else { '0' }
|
||||||
|
|
||||||
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $drive.Filters
|
||||||
|
|
||||||
|
@"
|
||||||
|
<Drive clsid="{935D1B74-9CB8-4e3c-9914-7DD559B7A417}" name="${letter}:" image="$image" changed="$timestamp" uid="$uid" userContext="1" removePolicy="0">
|
||||||
|
<Properties action="$action" thisDrive="NOCHANGE" allDrives="NOCHANGE" userName="" path="$path" label="$label" persistent="$persistent" useLetter="1" letter="$letter"/>$filterBlock
|
||||||
|
</Drive>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Drives clsid="{8FDDCC1A-0C3C-43cd-A6B4-71A6DF20DA8C}">
|
||||||
|
$($itemsXml -join "`n")
|
||||||
|
</Drives>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-EnvironmentVariableXml {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Generates EnvironmentVariables.xml GPP content.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[array]$Variables,
|
||||||
|
|
||||||
|
[string]$Scope = 'Machine'
|
||||||
|
)
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
$esc = [System.Security.SecurityElement]
|
||||||
|
$userContext = if ($Scope -eq 'User') { '1' } else { '0' }
|
||||||
|
$user = if ($Scope -eq 'User') { '1' } else { '0' }
|
||||||
|
|
||||||
|
$itemsXml = foreach ($var in $Variables) {
|
||||||
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
||||||
|
$action = $Script:GppActionCode[$var.Action]
|
||||||
|
$image = $Script:GppActionImage[$var.Action]
|
||||||
|
$name = $esc::Escape($var.Name)
|
||||||
|
$value = $esc::Escape($var.Value)
|
||||||
|
|
||||||
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $var.Filters
|
||||||
|
|
||||||
|
@"
|
||||||
|
<EnvironmentVariable clsid="{B1A3FA3F-E5D1-41ea-88D1-55CE96B6C78B}" name="$name" image="$image" changed="$timestamp" uid="$uid" userContext="$userContext">
|
||||||
|
<Properties action="$action" name="$name" value="$value" user="$user"/>$filterBlock
|
||||||
|
</EnvironmentVariable>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<EnvironmentVariables clsid="{D76B9641-3288-4f75-942D-087DE603E3EA}">
|
||||||
|
$($itemsXml -join "`n")
|
||||||
|
</EnvironmentVariables>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-ServiceXml {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Generates Services.xml GPP content for NTService entries.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[array]$Services
|
||||||
|
)
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
$esc = [System.Security.SecurityElement]
|
||||||
|
$startupMap = @{
|
||||||
|
Automatic = 'AUTOMATIC'
|
||||||
|
Manual = 'MANUAL'
|
||||||
|
Disabled = 'DISABLED'
|
||||||
|
}
|
||||||
|
|
||||||
|
$itemsXml = foreach ($svc in $Services) {
|
||||||
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
||||||
|
$action = $Script:GppActionCode[$svc.Action]
|
||||||
|
$image = $Script:GppActionImage[$svc.Action]
|
||||||
|
$serviceName = $esc::Escape($svc.ServiceName)
|
||||||
|
$startupType = $startupMap[$svc.StartupType]
|
||||||
|
if (-not $startupType) { $startupType = 'NOCHANGE' }
|
||||||
|
|
||||||
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $svc.Filters
|
||||||
|
|
||||||
|
@"
|
||||||
|
<NTService clsid="{AB6F0B67-D4E1-4f59-A9E5-F6BD3A72F4FF}" name="$serviceName" image="$image" changed="$timestamp" uid="$uid" userContext="0" removePolicy="0">
|
||||||
|
<Properties startupType="$startupType" serviceName="$serviceName" serviceAction="NONE" timeout="30"/>$filterBlock
|
||||||
|
</NTService>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<NTServices clsid="{2CFB484A-4E86-4eb1-8B6A-E1535488BDBF}">
|
||||||
|
$($itemsXml -join "`n")
|
||||||
|
</NTServices>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-PrinterXml {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Generates Printers.xml GPP content for shared printer entries.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[array]$Printers
|
||||||
|
)
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
$esc = [System.Security.SecurityElement]
|
||||||
|
|
||||||
|
$itemsXml = foreach ($printer in $Printers) {
|
||||||
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
||||||
|
$action = $Script:GppActionCode[$printer.Action]
|
||||||
|
$image = $Script:GppActionImage[$printer.Action]
|
||||||
|
$path = $esc::Escape($printer.Path)
|
||||||
|
# Derive name from last segment of UNC path
|
||||||
|
$name = ($printer.Path -split '\\' | Where-Object { $_ })[-1]
|
||||||
|
$default = if ($printer.Default) { '1' } else { '0' }
|
||||||
|
$skipLocal = if ($printer.SkipLocal) { '1' } else { '0' }
|
||||||
|
$comment = if ($printer.Comment) { $esc::Escape($printer.Comment) } else { '' }
|
||||||
|
$location = if ($printer.Location) { $esc::Escape($printer.Location) } else { '' }
|
||||||
|
|
||||||
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $printer.Filters
|
||||||
|
|
||||||
|
@"
|
||||||
|
<SharedPrinter clsid="{9A5E9697-9095-436d-A0EE-4D128FDFBCE5}" name="$name" image="$image" changed="$timestamp" uid="$uid" userContext="1" removePolicy="0">
|
||||||
|
<Properties action="$action" path="$path" comment="$comment" location="$location" default="$default" skipLocal="$skipLocal" deleteAll="0" persistent="0" deleteMaps="0" port=""/>$filterBlock
|
||||||
|
</SharedPrinter>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Printers clsid="{1F577D12-3D1B-471e-A1B7-060317597B9C}">
|
||||||
|
$($itemsXml -join "`n")
|
||||||
|
</Printers>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-ShortcutXml {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Generates Shortcuts.xml GPP content for shortcut entries.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[array]$Shortcuts,
|
||||||
|
|
||||||
|
[string]$Scope = 'User'
|
||||||
|
)
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
$esc = [System.Security.SecurityElement]
|
||||||
|
$userContext = if ($Scope -eq 'User') { '1' } else { '0' }
|
||||||
|
|
||||||
|
$itemsXml = foreach ($shortcut in $Shortcuts) {
|
||||||
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
||||||
|
$action = $Script:GppActionCode[$shortcut.Action]
|
||||||
|
$image = $Script:GppActionImage[$shortcut.Action]
|
||||||
|
$name = $esc::Escape($shortcut.Name)
|
||||||
|
$targetType = if ($shortcut.TargetType) { $shortcut.TargetType } else { 'FILESYSTEM' }
|
||||||
|
$targetPath = $esc::Escape($shortcut.TargetPath)
|
||||||
|
$shortcutPath = $esc::Escape($shortcut.ShortcutPath)
|
||||||
|
$arguments = if ($shortcut.Arguments) { $esc::Escape($shortcut.Arguments) } else { '' }
|
||||||
|
$startIn = if ($shortcut.StartIn) { $esc::Escape($shortcut.StartIn) } else { '' }
|
||||||
|
$comment = if ($shortcut.Comment) { $esc::Escape($shortcut.Comment) } else { '' }
|
||||||
|
$iconPath = if ($shortcut.IconPath) { $esc::Escape($shortcut.IconPath) } else { '' }
|
||||||
|
$iconIndex = if ($shortcut.IconIndex) { $shortcut.IconIndex } else { '0' }
|
||||||
|
|
||||||
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $shortcut.Filters
|
||||||
|
|
||||||
|
@"
|
||||||
|
<Shortcut clsid="{4F2F7C55-2790-433e-8127-0739D1CFA327}" name="$name" image="$image" changed="$timestamp" uid="$uid" userContext="$userContext" removePolicy="0">
|
||||||
|
<Properties action="$action" targetType="$targetType" targetPath="$targetPath" shortcutPath="$shortcutPath" arguments="$arguments" startIn="$startIn" comment="$comment" shortcutKey="0" iconPath="$iconPath" iconIndex="$iconIndex" window="" pidl=""/>$filterBlock
|
||||||
|
</Shortcut>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Shortcuts clsid="{872ECB34-B2EC-401b-A585-D32574AA90EE}">
|
||||||
|
$($itemsXml -join "`n")
|
||||||
|
</Shortcuts>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-FileXml {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Generates Files.xml GPP content for file copy/replace entries.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[array]$Files,
|
||||||
|
|
||||||
|
[string]$Scope = 'Machine'
|
||||||
|
)
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
$esc = [System.Security.SecurityElement]
|
||||||
|
$userContext = if ($Scope -eq 'User') { '1' } else { '0' }
|
||||||
|
|
||||||
|
$itemsXml = foreach ($file in $Files) {
|
||||||
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
||||||
|
$action = $Script:GppActionCode[$file.Action]
|
||||||
|
$image = $Script:GppActionImage[$file.Action]
|
||||||
|
$fromPath = $esc::Escape($file.FromPath)
|
||||||
|
$targetPath = $esc::Escape($file.TargetPath)
|
||||||
|
$readOnly = if ($file.ReadOnly) { '1' } else { '0' }
|
||||||
|
$hidden = if ($file.Hidden) { '1' } else { '0' }
|
||||||
|
|
||||||
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $file.Filters
|
||||||
|
|
||||||
|
@"
|
||||||
|
<File clsid="{50BE44C8-567A-4ed1-B1D0-9234FE1F38AF}" name="$targetPath" image="$image" changed="$timestamp" uid="$uid" userContext="$userContext" removePolicy="0">
|
||||||
|
<Properties action="$action" fromPath="$fromPath" targetPath="$targetPath" readOnly="$readOnly" archive="1" hidden="$hidden" suppress="1"/>$filterBlock
|
||||||
|
</File>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Files clsid="{215B2E53-57CE-475c-80FE-9EEC14635851}">
|
||||||
|
$($itemsXml -join "`n")
|
||||||
|
</Files>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-NetworkShareXml {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Generates NetworkShares.xml GPP content for network share entries.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[array]$Shares
|
||||||
|
)
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
$esc = [System.Security.SecurityElement]
|
||||||
|
|
||||||
|
$itemsXml = foreach ($share in $Shares) {
|
||||||
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
||||||
|
$action = $Script:GppActionCode[$share.Action]
|
||||||
|
$image = $Script:GppActionImage[$share.Action]
|
||||||
|
$name = $esc::Escape($share.Name)
|
||||||
|
$path = $esc::Escape($share.Path)
|
||||||
|
$comment = if ($share.Comment) { $esc::Escape($share.Comment) } else { '' }
|
||||||
|
$allRegular = if ($share.AllRegular) { $share.AllRegular } else { '' }
|
||||||
|
$allHidden = if ($share.AllHidden) { $share.AllHidden } else { '' }
|
||||||
|
$allAdminDrive = if ($share.AllAdminDrive) { $share.AllAdminDrive } else { '' }
|
||||||
|
$limitUsers = if ($share.LimitUsers) { $share.LimitUsers } else { '0' }
|
||||||
|
|
||||||
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $share.Filters
|
||||||
|
|
||||||
|
@"
|
||||||
|
<NetShare clsid="{2888C5E7-94FC-4739-90AA-2C1536D68BC0}" name="$name" image="$image" changed="$timestamp" uid="$uid" userContext="0" removePolicy="0">
|
||||||
|
<Properties action="$action" name="$name" path="$path" comment="$comment" allRegular="$allRegular" allHidden="$allHidden" allAdminDrive="$allAdminDrive" limitUsers="$limitUsers"/>$filterBlock
|
||||||
|
</NetShare>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<NetworkShareSettings clsid="{520870D8-A6E7-47e8-A8D8-E6A4E76EAEC2}">
|
||||||
|
$($itemsXml -join "`n")
|
||||||
|
</NetworkShareSettings>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-RegistryItemXml {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Generates Registry.xml GPP content for registry preference items.
|
||||||
|
These are GPP Registry items (not Administrative Templates).
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[array]$Items,
|
||||||
|
|
||||||
|
[string]$Scope = 'Machine'
|
||||||
|
)
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
$esc = [System.Security.SecurityElement]
|
||||||
|
$userContext = if ($Scope -eq 'User') { '1' } else { '0' }
|
||||||
|
|
||||||
|
$itemsXml = foreach ($item in $Items) {
|
||||||
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
||||||
|
$action = $Script:GppActionCode[$item.Action]
|
||||||
|
$image = $Script:GppActionImage[$item.Action]
|
||||||
|
$hive = $esc::Escape($item.Hive)
|
||||||
|
$key = $esc::Escape($item.Key)
|
||||||
|
$name = if ($item.Name) { $esc::Escape($item.Name) } else { '' }
|
||||||
|
$type = if ($item.Type) { $item.Type } else { 'REG_SZ' }
|
||||||
|
$value = if ($item.Value) { $esc::Escape($item.Value) } else { '' }
|
||||||
|
$displayName = if ($name) { $name } else { '(Default)' }
|
||||||
|
|
||||||
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $item.Filters
|
||||||
|
|
||||||
|
@"
|
||||||
|
<Registry clsid="{9CD4B2F4-923D-47f5-A062-E897DD1DAD50}" name="$displayName" image="$image" changed="$timestamp" uid="$uid" userContext="$userContext" removePolicy="0">
|
||||||
|
<Properties action="$action" hive="$hive" key="$key" name="$name" type="$type" value="$value" displayDecimal="0" default="0"/>$filterBlock
|
||||||
|
</Registry>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RegistrySettings clsid="{A3CCFC41-DFDB-43a5-8D26-0FE8B954DA51}">
|
||||||
|
$($itemsXml -join "`n")
|
||||||
|
</RegistrySettings>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-LocalGroupXml {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Generates Groups.xml GPP content for local user/group management.
|
||||||
|
Supports ADD/REMOVE individual members (unlike RestrictedGroups
|
||||||
|
which enforces exact membership).
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[array]$Groups
|
||||||
|
)
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
$esc = [System.Security.SecurityElement]
|
||||||
|
|
||||||
|
$itemsXml = foreach ($group in $Groups) {
|
||||||
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
||||||
|
$action = $Script:GppActionCode[$group.Action]
|
||||||
|
$image = $Script:GppActionImage[$group.Action]
|
||||||
|
$groupName = $esc::Escape($group.GroupName)
|
||||||
|
$newName = if ($group.NewName) { $esc::Escape($group.NewName) } else { '' }
|
||||||
|
$description = if ($group.Description) { $esc::Escape($group.Description) } else { '' }
|
||||||
|
$deleteAllUsers = if ($group.DeleteAllUsers) { '1' } else { '0' }
|
||||||
|
$deleteAllGroups = if ($group.DeleteAllGroups) { '1' } else { '0' }
|
||||||
|
$removeAccounts = if ($group.RemoveAccounts) { '1' } else { '0' }
|
||||||
|
|
||||||
|
# Build <Members> block
|
||||||
|
$membersXml = ''
|
||||||
|
if ($group.Members -and $group.Members.Count -gt 0) {
|
||||||
|
$memberLines = foreach ($m in $group.Members) {
|
||||||
|
$memberName = $esc::Escape($m.Name)
|
||||||
|
$memberAction = $m.Action.ToUpper()
|
||||||
|
$sid = ''
|
||||||
|
try {
|
||||||
|
$ntAccount = New-Object System.Security.Principal.NTAccount($m.Name)
|
||||||
|
$sid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
||||||
|
} catch {
|
||||||
|
Write-Host " [WARN] Cannot resolve '$($m.Name)' to SID -- leaving empty" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
" <Member name=`"$memberName`" action=`"$memberAction`" sid=`"$sid`"/>"
|
||||||
|
}
|
||||||
|
$membersXml = "`n <Members>`n$($memberLines -join "`n")`n </Members>"
|
||||||
|
}
|
||||||
|
|
||||||
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $group.Filters
|
||||||
|
|
||||||
|
@"
|
||||||
|
<Group clsid="{6D4A79E4-529C-4481-ABD0-F5BD7EA93BA7}" name="$groupName" image="$image" changed="$timestamp" uid="$uid" userContext="0" removePolicy="0">
|
||||||
|
<Properties action="$action" groupName="$groupName" newName="$newName" description="$description" deleteAllUsers="$deleteAllUsers" deleteAllGroups="$deleteAllGroups" removeAccounts="$removeAccounts">$membersXml
|
||||||
|
</Properties>$filterBlock
|
||||||
|
</Group>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
return @"
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Groups clsid="{3125E937-EB16-4b4c-9934-544FC6D24D26}">
|
||||||
|
$($itemsXml -join "`n")
|
||||||
|
</Groups>
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-GPOPreferences {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Writes Group Policy Preferences XML files to SYSVOL.
|
||||||
|
Supports 10 GPP types: ScheduledTasks, DriveMaps, EnvironmentVariables,
|
||||||
|
Services, Printers, Shortcuts, Files, NetworkShares, RegistryItems,
|
||||||
|
LocalUsersAndGroups.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$Preferences,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
|
||||||
|
$utf8Bom = [System.Text.UTF8Encoding]::new($true)
|
||||||
|
|
||||||
|
foreach ($typeName in $Preferences.Keys) {
|
||||||
|
$typeInfo = $Script:GppTypeInfo[$typeName]
|
||||||
|
if (-not $typeInfo) {
|
||||||
|
Write-Host " [WARN] Unknown preference type: $typeName" -ForegroundColor Yellow
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $Preferences[$typeName]
|
||||||
|
if (-not $items -or $items.Count -eq 0) { continue }
|
||||||
|
|
||||||
|
# Group items by scope
|
||||||
|
$byScope = @{ Machine = @(); User = @() }
|
||||||
|
foreach ($item in $items) {
|
||||||
|
$scope = switch ($typeName) {
|
||||||
|
'DriveMaps' { 'User' }
|
||||||
|
'Printers' { 'User' }
|
||||||
|
'Services' { 'Machine' }
|
||||||
|
'NetworkShares' { 'Machine' }
|
||||||
|
'LocalUsersAndGroups' { 'Machine' }
|
||||||
|
default { if ($item.Scope -eq 'User') { 'User' } else { 'Machine' } }
|
||||||
|
}
|
||||||
|
$byScope[$scope] += $item
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($scope in @('Machine', 'User')) {
|
||||||
|
$scopeItems = $byScope[$scope]
|
||||||
|
if ($scopeItems.Count -eq 0) { continue }
|
||||||
|
|
||||||
|
# Generate XML
|
||||||
|
$xml = switch ($typeName) {
|
||||||
|
'ScheduledTasks' { ConvertTo-ScheduledTaskXml -Tasks $scopeItems -Scope $scope }
|
||||||
|
'DriveMaps' { ConvertTo-DriveMapXml -Drives $scopeItems }
|
||||||
|
'EnvironmentVariables' { ConvertTo-EnvironmentVariableXml -Variables $scopeItems -Scope $scope }
|
||||||
|
'Services' { ConvertTo-ServiceXml -Services $scopeItems }
|
||||||
|
'Printers' { ConvertTo-PrinterXml -Printers $scopeItems }
|
||||||
|
'Shortcuts' { ConvertTo-ShortcutXml -Shortcuts $scopeItems -Scope $scope }
|
||||||
|
'Files' { ConvertTo-FileXml -Files $scopeItems -Scope $scope }
|
||||||
|
'NetworkShares' { ConvertTo-NetworkShareXml -Shares $scopeItems }
|
||||||
|
'RegistryItems' { ConvertTo-RegistryItemXml -Items $scopeItems -Scope $scope }
|
||||||
|
'LocalUsersAndGroups' { ConvertTo-LocalGroupXml -Groups $scopeItems }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write to SYSVOL
|
||||||
|
$prefDir = Join-Path $sysvolPath "$scope\Preferences\$($typeInfo.SysvolDir)"
|
||||||
|
if (-not (Test-Path $prefDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $prefDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
$xmlPath = Join-Path $prefDir $typeInfo.FileName
|
||||||
|
[System.IO.File]::WriteAllText($xmlPath, $xml, $utf8Bom)
|
||||||
|
Write-Host " Written: $scope\Preferences\$($typeInfo.SysvolDir)\$($typeInfo.FileName) ($($scopeItems.Count) item(s))" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Register CSE extension GUIDs
|
||||||
|
Add-GPOExtensionGuids -GPOName $GPOName -CseGuid $typeInfo.CseGuid -ToolGuid $Script:GppToolGuid -Scope $scope -Domain $Domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-GPOPreferences {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compares desired Group Policy Preferences against what's currently
|
||||||
|
deployed in SYSVOL. Checks XML file existence and item presence by name.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$Preferences,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
|
||||||
|
$diffs = @()
|
||||||
|
|
||||||
|
Write-Host " Comparing preferences..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
foreach ($typeName in $Preferences.Keys) {
|
||||||
|
$typeInfo = $Script:GppTypeInfo[$typeName]
|
||||||
|
if (-not $typeInfo) { continue }
|
||||||
|
|
||||||
|
$items = $Preferences[$typeName]
|
||||||
|
if (-not $items -or $items.Count -eq 0) { continue }
|
||||||
|
|
||||||
|
# Group items by scope
|
||||||
|
$byScope = @{ Machine = @(); User = @() }
|
||||||
|
foreach ($item in $items) {
|
||||||
|
$scope = switch ($typeName) {
|
||||||
|
'DriveMaps' { 'User' }
|
||||||
|
'Printers' { 'User' }
|
||||||
|
'Services' { 'Machine' }
|
||||||
|
'NetworkShares' { 'Machine' }
|
||||||
|
'LocalUsersAndGroups' { 'Machine' }
|
||||||
|
default { if ($item.Scope -eq 'User') { 'User' } else { 'Machine' } }
|
||||||
|
}
|
||||||
|
$byScope[$scope] += $item
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($scope in @('Machine', 'User')) {
|
||||||
|
$scopeItems = $byScope[$scope]
|
||||||
|
if ($scopeItems.Count -eq 0) { continue }
|
||||||
|
|
||||||
|
$xmlPath = Join-Path $sysvolPath "$scope\Preferences\$($typeInfo.SysvolDir)\$($typeInfo.FileName)"
|
||||||
|
|
||||||
|
if (-not (Test-Path $xmlPath)) {
|
||||||
|
Write-Host " [DRIFT] Missing: $scope\Preferences\$($typeInfo.SysvolDir)\$($typeInfo.FileName)" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'Preference'
|
||||||
|
PrefType = $typeName
|
||||||
|
Scope = $scope
|
||||||
|
Item = '(entire file)'
|
||||||
|
Status = 'Missing'
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse existing XML and check for each desired item
|
||||||
|
try {
|
||||||
|
[xml]$existingXml = Get-Content $xmlPath -Raw
|
||||||
|
|
||||||
|
foreach ($item in $scopeItems) {
|
||||||
|
$itemName = $item.($typeInfo.NameKey)
|
||||||
|
|
||||||
|
# DriveMaps use "Z:" format in XML but settings use "Z"
|
||||||
|
$searchName = $itemName
|
||||||
|
if ($typeName -eq 'DriveMaps') {
|
||||||
|
$searchName = "$($itemName.TrimEnd(':')):"
|
||||||
|
}
|
||||||
|
|
||||||
|
$found = $existingXml.SelectNodes("//*[@name='$searchName']")
|
||||||
|
if (-not $found -or $found.Count -eq 0) {
|
||||||
|
Write-Host " [DRIFT] Missing $typeName item: $itemName" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'Preference'
|
||||||
|
PrefType = $typeName
|
||||||
|
Scope = $scope
|
||||||
|
Item = $itemName
|
||||||
|
Status = 'Missing item'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [OK] $typeName`: $itemName" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host " [DRIFT] Cannot parse: $($typeInfo.FileName) -- $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'Preference'
|
||||||
|
PrefType = $typeName
|
||||||
|
Scope = $scope
|
||||||
|
Item = '(parse error)'
|
||||||
|
Status = 'Invalid XML'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($diffs.Count -eq 0) {
|
||||||
|
Write-Host " [OK] All preferences match desired state" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [DRIFT] $($diffs.Count) preference difference(s) found" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
271
gpo/lib/GPOScripts.ps1
Normal file
271
gpo/lib/GPOScripts.ps1
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
# GPOScripts.ps1
|
||||||
|
# Startup/shutdown/logon/logoff script deployment to SYSVOL.
|
||||||
|
# Depends on: GPOCore.ps1 (Get-GPOSysvolPath, Add-GPOExtensionGuids)
|
||||||
|
|
||||||
|
function Set-GPOScripts {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Deploys startup/shutdown/logon/logoff scripts to a GPO's SYSVOL path
|
||||||
|
and generates the corresponding psscripts.ini / scripts.ini files.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$Scripts,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$SourceDir,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
|
||||||
|
|
||||||
|
# Map settings keys to SYSVOL paths and ini section names
|
||||||
|
$typeInfo = @{
|
||||||
|
MachineStartup = @{ Scope = 'Machine'; SubDir = 'Startup'; Section = 'Startup' }
|
||||||
|
MachineShutdown = @{ Scope = 'Machine'; SubDir = 'Shutdown'; Section = 'Shutdown' }
|
||||||
|
UserLogon = @{ Scope = 'User'; SubDir = 'Logon'; Section = 'Logon' }
|
||||||
|
UserLogoff = @{ Scope = 'User'; SubDir = 'Logoff'; Section = 'Logoff' }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Script CSE GUID and tool extension GUIDs
|
||||||
|
$scriptCseGuid = '{42B5FAAE-6536-11D2-AE5A-0000F87571E3}'
|
||||||
|
$machineToolGuid = '{40B6664F-4972-11D1-A7CA-0000F87571E3}'
|
||||||
|
$userToolGuid = '{40B66650-4972-11D1-A7CA-0000F87571E3}'
|
||||||
|
|
||||||
|
# Group work by scope (Machine / User) for ini file generation
|
||||||
|
$scopeWork = @{
|
||||||
|
Machine = @{ PsSections = [ordered]@{}; CmdSections = [ordered]@{} }
|
||||||
|
User = @{ PsSections = [ordered]@{}; CmdSections = [ordered]@{} }
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($type in $Scripts.Keys) {
|
||||||
|
$info = $typeInfo[$type]
|
||||||
|
if (-not $info) {
|
||||||
|
Write-Host " [WARN] Unknown script type: $type" -ForegroundColor Yellow
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope = $info.Scope
|
||||||
|
$section = $info.Section
|
||||||
|
$scriptDir = Join-Path $sysvolPath "$scope\Scripts\$($info.SubDir)"
|
||||||
|
|
||||||
|
if (-not (Test-Path $scriptDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $scriptDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$psEntries = @()
|
||||||
|
$cmdEntries = @()
|
||||||
|
|
||||||
|
foreach ($script in $Scripts[$type]) {
|
||||||
|
$sourcePath = Join-Path $SourceDir $script.Source
|
||||||
|
$fileName = Split-Path $script.Source -Leaf
|
||||||
|
$destPath = Join-Path $scriptDir $fileName
|
||||||
|
$params = if ($script.Parameters) { $script.Parameters } else { '' }
|
||||||
|
|
||||||
|
Copy-Item -Path $sourcePath -Destination $destPath -Force
|
||||||
|
Write-Host " Copied: $fileName -> $scope\Scripts\$($info.SubDir)\" -ForegroundColor Green
|
||||||
|
|
||||||
|
$entry = @{ CmdLine = $fileName; Parameters = $params }
|
||||||
|
if ($fileName -match '\.ps1$') {
|
||||||
|
$psEntries += $entry
|
||||||
|
} else {
|
||||||
|
$cmdEntries += $entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($psEntries.Count -gt 0) {
|
||||||
|
$scopeWork[$scope].PsSections[$section] = $psEntries
|
||||||
|
}
|
||||||
|
if ($cmdEntries.Count -gt 0) {
|
||||||
|
$scopeWork[$scope].CmdSections[$section] = $cmdEntries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate ini files per scope
|
||||||
|
foreach ($scope in @('Machine', 'User')) {
|
||||||
|
$work = $scopeWork[$scope]
|
||||||
|
$scriptsDir = Join-Path $sysvolPath "$scope\Scripts"
|
||||||
|
|
||||||
|
# psscripts.ini -- PowerShell scripts
|
||||||
|
if ($work.PsSections.Count -gt 0) {
|
||||||
|
if (-not (Test-Path $scriptsDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $scriptsDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$sb = [System.Text.StringBuilder]::new()
|
||||||
|
foreach ($section in $work.PsSections.Keys) {
|
||||||
|
[void]$sb.AppendLine("[$section]")
|
||||||
|
$idx = 0
|
||||||
|
foreach ($entry in $work.PsSections[$section]) {
|
||||||
|
[void]$sb.AppendLine("${idx}CmdLine=$($entry.CmdLine)")
|
||||||
|
[void]$sb.AppendLine("${idx}Parameters=$($entry.Parameters)")
|
||||||
|
$idx++
|
||||||
|
}
|
||||||
|
[void]$sb.AppendLine('')
|
||||||
|
}
|
||||||
|
|
||||||
|
$iniPath = Join-Path $scriptsDir 'psscripts.ini'
|
||||||
|
[System.IO.File]::WriteAllText($iniPath, $sb.ToString(), [System.Text.Encoding]::Unicode)
|
||||||
|
Write-Host " Written: $scope\Scripts\psscripts.ini" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# scripts.ini -- non-PowerShell scripts (.bat, .cmd, .exe)
|
||||||
|
if ($work.CmdSections.Count -gt 0) {
|
||||||
|
if (-not (Test-Path $scriptsDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $scriptsDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$sb = [System.Text.StringBuilder]::new()
|
||||||
|
foreach ($section in $work.CmdSections.Keys) {
|
||||||
|
[void]$sb.AppendLine("[$section]")
|
||||||
|
$idx = 0
|
||||||
|
foreach ($entry in $work.CmdSections[$section]) {
|
||||||
|
[void]$sb.AppendLine("${idx}CmdLine=$($entry.CmdLine)")
|
||||||
|
[void]$sb.AppendLine("${idx}Parameters=$($entry.Parameters)")
|
||||||
|
$idx++
|
||||||
|
}
|
||||||
|
[void]$sb.AppendLine('')
|
||||||
|
}
|
||||||
|
|
||||||
|
$iniPath = Join-Path $scriptsDir 'scripts.ini'
|
||||||
|
[System.IO.File]::WriteAllText($iniPath, $sb.ToString(), [System.Text.Encoding]::Unicode)
|
||||||
|
Write-Host " Written: $scope\Scripts\scripts.ini" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update CSE extension GUIDs in AD
|
||||||
|
if ($work.PsSections.Count -gt 0 -or $work.CmdSections.Count -gt 0) {
|
||||||
|
$toolGuid = if ($scope -eq 'Machine') { $machineToolGuid } else { $userToolGuid }
|
||||||
|
Add-GPOExtensionGuids -GPOName $GPOName -CseGuid $scriptCseGuid -ToolGuid $toolGuid -Scope $scope -Domain $Domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-GPOScripts {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compares desired scripts against what's currently deployed in a GPO's
|
||||||
|
SYSVOL path. Returns diff objects for missing or changed scripts.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$Scripts,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$SourceDir,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
|
||||||
|
|
||||||
|
$typeInfo = @{
|
||||||
|
MachineStartup = @{ Scope = 'Machine'; SubDir = 'Startup' }
|
||||||
|
MachineShutdown = @{ Scope = 'Machine'; SubDir = 'Shutdown' }
|
||||||
|
UserLogon = @{ Scope = 'User'; SubDir = 'Logon' }
|
||||||
|
UserLogoff = @{ Scope = 'User'; SubDir = 'Logoff' }
|
||||||
|
}
|
||||||
|
|
||||||
|
$diffs = @()
|
||||||
|
|
||||||
|
Write-Host " Comparing scripts..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
foreach ($type in $Scripts.Keys) {
|
||||||
|
$info = $typeInfo[$type]
|
||||||
|
if (-not $info) { continue }
|
||||||
|
|
||||||
|
$scriptDir = Join-Path $sysvolPath "$($info.Scope)\Scripts\$($info.SubDir)"
|
||||||
|
|
||||||
|
foreach ($script in $Scripts[$type]) {
|
||||||
|
$fileName = Split-Path $script.Source -Leaf
|
||||||
|
$sourcePath = Join-Path $SourceDir $script.Source
|
||||||
|
$destPath = Join-Path $scriptDir $fileName
|
||||||
|
|
||||||
|
if (-not (Test-Path $destPath)) {
|
||||||
|
Write-Host " [DRIFT] Missing: $fileName in $($info.Scope)\Scripts\$($info.SubDir)\" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'Script'
|
||||||
|
ScriptType = $type
|
||||||
|
FileName = $fileName
|
||||||
|
Status = 'Missing'
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compare file content by hash
|
||||||
|
$sourceHash = (Get-FileHash -Path $sourcePath -Algorithm SHA256).Hash
|
||||||
|
$destHash = (Get-FileHash -Path $destPath -Algorithm SHA256).Hash
|
||||||
|
|
||||||
|
if ($sourceHash -ne $destHash) {
|
||||||
|
Write-Host " [DRIFT] Changed: $fileName in $($info.Scope)\Scripts\$($info.SubDir)\" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'Script'
|
||||||
|
ScriptType = $type
|
||||||
|
FileName = $fileName
|
||||||
|
Status = 'Content changed'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [OK] $fileName" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check psscripts.ini / scripts.ini existence per scope
|
||||||
|
$scopeHasPs = @{ Machine = $false; User = $false }
|
||||||
|
$scopeHasCmd = @{ Machine = $false; User = $false }
|
||||||
|
|
||||||
|
foreach ($type in $Scripts.Keys) {
|
||||||
|
$info = $typeInfo[$type]
|
||||||
|
if (-not $info) { continue }
|
||||||
|
foreach ($script in $Scripts[$type]) {
|
||||||
|
$fileName = Split-Path $script.Source -Leaf
|
||||||
|
if ($fileName -match '\.ps1$') { $scopeHasPs[$info.Scope] = $true }
|
||||||
|
else { $scopeHasCmd[$info.Scope] = $true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($scope in @('Machine', 'User')) {
|
||||||
|
$scriptsDir = Join-Path $sysvolPath "$scope\Scripts"
|
||||||
|
|
||||||
|
if ($scopeHasPs[$scope]) {
|
||||||
|
$iniPath = Join-Path $scriptsDir 'psscripts.ini'
|
||||||
|
if (-not (Test-Path $iniPath)) {
|
||||||
|
Write-Host " [DRIFT] Missing: $scope\Scripts\psscripts.ini" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'ScriptIni'
|
||||||
|
Scope = $scope
|
||||||
|
FileName = 'psscripts.ini'
|
||||||
|
Status = 'Missing'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scopeHasCmd[$scope]) {
|
||||||
|
$iniPath = Join-Path $scriptsDir 'scripts.ini'
|
||||||
|
if (-not (Test-Path $iniPath)) {
|
||||||
|
Write-Host " [DRIFT] Missing: $scope\Scripts\scripts.ini" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'ScriptIni'
|
||||||
|
Scope = $scope
|
||||||
|
FileName = 'scripts.ini'
|
||||||
|
Status = 'Missing'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($diffs.Count -eq 0) {
|
||||||
|
Write-Host " [OK] All scripts match desired state" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [DRIFT] $($diffs.Count) script difference(s) found" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
181
gpo/lib/GPOWdac.ps1
Normal file
181
gpo/lib/GPOWdac.ps1
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
# GPOWdac.ps1
|
||||||
|
# Lightweight WDAC (Windows Defender Application Control) policy deployment via GPO.
|
||||||
|
# Copies .p7b policy to SYSVOL and sets the registry pointer.
|
||||||
|
# Depends on: GPOCore.ps1 (Get-GPOSysvolPath)
|
||||||
|
|
||||||
|
function Set-GPOWdacPolicy {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Deploys a WDAC policy file to a GPO.
|
||||||
|
If source is .xml, converts to .p7b with ConvertFrom-CIPolicy.
|
||||||
|
Copies .p7b to SYSVOL and sets the DeployConfigCIPolicy registry key.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$WDACPolicy,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$SourceDir,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host " Applying WDAC policy..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$policyFile = $WDACPolicy.PolicyFile
|
||||||
|
$sourcePath = Join-Path $SourceDir $policyFile
|
||||||
|
|
||||||
|
if (-not (Test-Path $sourcePath)) {
|
||||||
|
throw "WDAC policy file not found: $sourcePath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
|
||||||
|
$deviceGuardDir = Join-Path $sysvolPath 'Machine\Microsoft\Windows\DeviceGuard'
|
||||||
|
|
||||||
|
if (-not (Test-Path $deviceGuardDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $deviceGuardDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = [System.IO.Path]::GetExtension($sourcePath).ToLower()
|
||||||
|
|
||||||
|
if ($extension -eq '.xml') {
|
||||||
|
# Convert XML to .p7b
|
||||||
|
$destPath = Join-Path $deviceGuardDir 'SIPolicy.p7b'
|
||||||
|
ConvertFrom-CIPolicy -XmlFilePath $sourcePath -BinaryFilePath $destPath | Out-Null
|
||||||
|
Write-Host " Converted $policyFile -> SIPolicy.p7b" -ForegroundColor Green
|
||||||
|
} elseif ($extension -eq '.p7b') {
|
||||||
|
# Copy directly
|
||||||
|
$destPath = Join-Path $deviceGuardDir 'SIPolicy.p7b'
|
||||||
|
Copy-Item -Path $sourcePath -Destination $destPath -Force
|
||||||
|
Write-Host " Copied $policyFile -> SIPolicy.p7b" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
throw "WDAC policy file must be .xml or .p7b, got: $extension"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set registry key to enable WDAC via GPO
|
||||||
|
$regKey = 'HKLM\SOFTWARE\Policies\Microsoft\Windows\DeviceGuard'
|
||||||
|
Set-GPRegistryValue -Name $GPOName -Key $regKey -ValueName 'DeployConfigCIPolicy' `
|
||||||
|
-Type DWord -Value 1 -Domain $Domain | Out-Null
|
||||||
|
|
||||||
|
Write-Host " [OK] WDAC policy deployed to $GPOName" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-GPOWdacPolicy {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Compares desired WDAC policy against current GPO state.
|
||||||
|
Checks registry key and .p7b file existence/hash.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$WDACPolicy,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$SourceDir,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$diffs = @()
|
||||||
|
|
||||||
|
Write-Host " Comparing WDAC policy..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Check registry key
|
||||||
|
$regKey = 'HKLM\SOFTWARE\Policies\Microsoft\Windows\DeviceGuard'
|
||||||
|
$currentRegValue = $null
|
||||||
|
try {
|
||||||
|
$regResult = Get-GPRegistryValue -Name $GPOName -Key $regKey -ValueName 'DeployConfigCIPolicy' `
|
||||||
|
-Domain $Domain -ErrorAction Stop
|
||||||
|
$currentRegValue = $regResult.Value
|
||||||
|
} catch {
|
||||||
|
$currentRegValue = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentRegValue -ne 1) {
|
||||||
|
$display = if ($null -eq $currentRegValue) { '(not set)' } else { $currentRegValue }
|
||||||
|
Write-Host " [DRIFT] DeployConfigCIPolicy: $display -> 1" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'WDAC'
|
||||||
|
Setting = 'DeployConfigCIPolicy'
|
||||||
|
Status = "Registry: $display -> 1"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [OK] DeployConfigCIPolicy = 1" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check .p7b file exists in SYSVOL
|
||||||
|
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
|
||||||
|
$p7bPath = Join-Path $sysvolPath 'Machine\Microsoft\Windows\DeviceGuard\SIPolicy.p7b'
|
||||||
|
|
||||||
|
if (-not (Test-Path $p7bPath)) {
|
||||||
|
Write-Host " [DRIFT] Missing: SIPolicy.p7b" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'WDAC'
|
||||||
|
Setting = 'SIPolicy.p7b'
|
||||||
|
Status = 'Missing'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# Compare file hashes if source exists
|
||||||
|
$policyFile = $WDACPolicy.PolicyFile
|
||||||
|
$sourcePath = Join-Path $SourceDir $policyFile
|
||||||
|
|
||||||
|
if (Test-Path $sourcePath) {
|
||||||
|
$sourceExt = [System.IO.Path]::GetExtension($sourcePath).ToLower()
|
||||||
|
|
||||||
|
if ($sourceExt -eq '.p7b') {
|
||||||
|
$sourceHash = (Get-FileHash -Path $sourcePath -Algorithm SHA256).Hash
|
||||||
|
$deployedHash = (Get-FileHash -Path $p7bPath -Algorithm SHA256).Hash
|
||||||
|
|
||||||
|
if ($sourceHash -ne $deployedHash) {
|
||||||
|
Write-Host " [DRIFT] SIPolicy.p7b hash mismatch (source updated)" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'WDAC'
|
||||||
|
Setting = 'SIPolicy.p7b'
|
||||||
|
Status = 'Hash mismatch'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [OK] SIPolicy.p7b hash matches" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# Source is .xml -- convert to temp .p7b and compare
|
||||||
|
$tempP7b = [System.IO.Path]::GetTempFileName()
|
||||||
|
try {
|
||||||
|
ConvertFrom-CIPolicy -XmlFilePath $sourcePath -BinaryFilePath $tempP7b | Out-Null
|
||||||
|
$sourceHash = (Get-FileHash -Path $tempP7b -Algorithm SHA256).Hash
|
||||||
|
$deployedHash = (Get-FileHash -Path $p7bPath -Algorithm SHA256).Hash
|
||||||
|
|
||||||
|
if ($sourceHash -ne $deployedHash) {
|
||||||
|
Write-Host " [DRIFT] SIPolicy.p7b hash mismatch (source XML updated)" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'WDAC'
|
||||||
|
Setting = 'SIPolicy.p7b'
|
||||||
|
Status = 'Hash mismatch'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [OK] SIPolicy.p7b matches source XML" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host " [WARN] Cannot convert source XML for comparison: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||||
|
} finally {
|
||||||
|
Remove-Item $tempP7b -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " [OK] SIPolicy.p7b exists (source file not found for hash comparison)" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($diffs.Count -eq 0) {
|
||||||
|
Write-Host " [OK] WDAC policy matches desired state" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [DRIFT] $($diffs.Count) WDAC difference(s) found" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
190
gpo/lib/GPOWmiFilter.ps1
Normal file
190
gpo/lib/GPOWmiFilter.ps1
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
# GPOWmiFilter.ps1
|
||||||
|
# WMI filter creation and GPO linking.
|
||||||
|
# No dependencies on GPOCore.ps1 (uses only AD cmdlets directly).
|
||||||
|
|
||||||
|
function Ensure-GPOWmiFilter {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Creates a WMI filter AD object if missing, then links it to the GPO.
|
||||||
|
Idempotent: skips creation if a filter with the same name exists,
|
||||||
|
updates the query or description if they have changed.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$FilterName,
|
||||||
|
|
||||||
|
[string]$Description = '',
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Query,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$domainDN = (Get-ADDomain -Server $Domain).DistinguishedName
|
||||||
|
$wmiContainer = "CN=SOM,CN=WMIPolicy,CN=System,$domainDN"
|
||||||
|
|
||||||
|
# Format the WQL query for msWMI-Parm2
|
||||||
|
# Format: 1;3;10;<queryLen>;WQL;root\CIMv2;<query>;
|
||||||
|
# 10 = length of 'root\CIMv2'
|
||||||
|
$queryLen = $Query.Length
|
||||||
|
$parm2 = "1;3;10;$queryLen;WQL;root\CIMv2;$Query;"
|
||||||
|
|
||||||
|
# Search for existing filter by name
|
||||||
|
$existing = Get-ADObject -SearchBase $wmiContainer -Filter { objectClass -eq 'msWMI-Som' } `
|
||||||
|
-Properties 'msWMI-Name', 'msWMI-Parm1', 'msWMI-Parm2', 'msWMI-ID' |
|
||||||
|
Where-Object { $_.'msWMI-Name' -eq $FilterName }
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$filterGuid = $existing.'msWMI-ID'
|
||||||
|
Write-Host " [OK] WMI filter exists: $FilterName ($filterGuid)" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Check if query or description need updating
|
||||||
|
$updates = @{}
|
||||||
|
if ($existing.'msWMI-Parm2' -ne $parm2) {
|
||||||
|
$updates['msWMI-Parm2'] = $parm2
|
||||||
|
}
|
||||||
|
if ($existing.'msWMI-Parm1' -ne $Description) {
|
||||||
|
$updates['msWMI-Parm1'] = $Description
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($updates.Count -gt 0) {
|
||||||
|
$timestamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddHHmmss.ffffff-000')
|
||||||
|
$updates['msWMI-ChangeDate'] = $timestamp
|
||||||
|
Set-ADObject $existing.DistinguishedName -Replace $updates
|
||||||
|
Write-Host " [UPDATED] WMI filter updated: $FilterName" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# Create new WMI filter
|
||||||
|
$filterGuid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
||||||
|
$timestamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddHHmmss.ffffff-000')
|
||||||
|
$author = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||||
|
|
||||||
|
$attributes = @{
|
||||||
|
'msWMI-Name' = $FilterName
|
||||||
|
'msWMI-Parm1' = $Description
|
||||||
|
'msWMI-Parm2' = $parm2
|
||||||
|
'msWMI-ID' = $filterGuid
|
||||||
|
'msWMI-Author' = $author
|
||||||
|
'msWMI-CreationDate' = $timestamp
|
||||||
|
'msWMI-ChangeDate' = $timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
New-ADObject -Name $filterGuid -Type 'msWMI-Som' -Path $wmiContainer `
|
||||||
|
-OtherAttributes $attributes
|
||||||
|
Write-Host " [CREATED] WMI filter: $FilterName ($filterGuid)" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# Link the WMI filter to the GPO
|
||||||
|
$gpo = Get-GPO -Name $GPOName -Domain $Domain -ErrorAction Stop
|
||||||
|
$gpoDN = "CN={$($gpo.Id)},CN=Policies,CN=System,$domainDN"
|
||||||
|
$linkValue = "[$Domain;$filterGuid;0]"
|
||||||
|
|
||||||
|
$currentLink = (Get-ADObject $gpoDN -Properties gPCWQLFilter).gPCWQLFilter
|
||||||
|
if ($currentLink -eq $linkValue) {
|
||||||
|
Write-Host " [OK] WMI filter linked to GPO: $GPOName" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Set-ADObject $gpoDN -Replace @{ gPCWQLFilter = $linkValue }
|
||||||
|
Write-Host " [LINKED] WMI filter '$FilterName' -> GPO: $GPOName" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-GPOWmiFilter {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Checks if a WMI filter exists with the correct query and is linked
|
||||||
|
to the GPO. Returns diff objects for any discrepancies.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$GPOName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$FilterName,
|
||||||
|
|
||||||
|
[string]$Description = '',
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Query,
|
||||||
|
|
||||||
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
$diffs = @()
|
||||||
|
$domainDN = (Get-ADDomain -Server $Domain).DistinguishedName
|
||||||
|
$wmiContainer = "CN=SOM,CN=WMIPolicy,CN=System,$domainDN"
|
||||||
|
|
||||||
|
$queryLen = $Query.Length
|
||||||
|
$parm2 = "1;3;10;$queryLen;WQL;root\CIMv2;$Query;"
|
||||||
|
|
||||||
|
Write-Host " Checking WMI filter..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Search for existing filter by name
|
||||||
|
$existing = Get-ADObject -SearchBase $wmiContainer -Filter { objectClass -eq 'msWMI-Som' } `
|
||||||
|
-Properties 'msWMI-Name', 'msWMI-Parm1', 'msWMI-Parm2', 'msWMI-ID' |
|
||||||
|
Where-Object { $_.'msWMI-Name' -eq $FilterName }
|
||||||
|
|
||||||
|
if (-not $existing) {
|
||||||
|
Write-Host " [DRIFT] WMI filter not found: $FilterName" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'WMIFilter'
|
||||||
|
Filter = $FilterName
|
||||||
|
Status = 'Missing'
|
||||||
|
}
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
|
|
||||||
|
$filterGuid = $existing.'msWMI-ID'
|
||||||
|
Write-Host " [OK] WMI filter exists: $FilterName" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Check query
|
||||||
|
if ($existing.'msWMI-Parm2' -ne $parm2) {
|
||||||
|
Write-Host " [DRIFT] WMI filter query differs" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'WMIFilterQuery'
|
||||||
|
Filter = $FilterName
|
||||||
|
Current = $existing.'msWMI-Parm2'
|
||||||
|
Desired = $parm2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check description
|
||||||
|
if ($existing.'msWMI-Parm1' -ne $Description) {
|
||||||
|
Write-Host " [DRIFT] WMI filter description differs" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'WMIFilterDescription'
|
||||||
|
Filter = $FilterName
|
||||||
|
Current = $existing.'msWMI-Parm1'
|
||||||
|
Desired = $Description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check link to GPO
|
||||||
|
$gpo = Get-GPO -Name $GPOName -Domain $Domain -ErrorAction Stop
|
||||||
|
$gpoDN = "CN={$($gpo.Id)},CN=Policies,CN=System,$domainDN"
|
||||||
|
$linkValue = "[$Domain;$filterGuid;0]"
|
||||||
|
$currentLink = (Get-ADObject $gpoDN -Properties gPCWQLFilter).gPCWQLFilter
|
||||||
|
|
||||||
|
if ($currentLink -eq $linkValue) {
|
||||||
|
Write-Host " [OK] WMI filter linked to GPO" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
$currentDisplay = if ($currentLink) { $currentLink } else { '(none)' }
|
||||||
|
Write-Host " [DRIFT] WMI filter not linked to GPO: $GPOName" -ForegroundColor Red
|
||||||
|
$diffs += [PSCustomObject]@{
|
||||||
|
Type = 'WMIFilterLink'
|
||||||
|
GPO = $GPOName
|
||||||
|
Filter = $FilterName
|
||||||
|
Current = $currentDisplay
|
||||||
|
Desired = $linkValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($diffs.Count -eq 0) {
|
||||||
|
Write-Host " [OK] WMI filter matches desired state" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs
|
||||||
|
}
|
||||||
115
gpo/servers-01/README.md
Normal file
115
gpo/servers-01/README.md
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
# Servers-01 GPO
|
||||||
|
|
||||||
|
Server hardening policy for domain-joined servers in the ExampleServers OU.
|
||||||
|
|
||||||
|
## Linked To
|
||||||
|
|
||||||
|
`OU=ExampleServers,DC=example,DC=internal`
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
Full audit, logging, and hardening baseline for servers. Compared to Workstations-01:
|
||||||
|
|
||||||
|
- **Full audit coverage** -- every category audits both success and failure, including process tracking and DS access
|
||||||
|
- **PowerShell transcription** -- complete session recording to `C:\PSlogs\Transcripts`
|
||||||
|
- **Module logging** -- all PowerShell modules logged
|
||||||
|
- **Command line in process creation** -- Event ID 4688 includes full command line
|
||||||
|
- **Larger event logs** -- 64/256 MB (matches AdminWorkstations-01)
|
||||||
|
- **Weekly updates** -- Sunday 3 AM instead of daily (minimize reboot disruption for services)
|
||||||
|
|
||||||
|
Compared to AdminWorkstations-01, this GPO does **not** include:
|
||||||
|
|
||||||
|
- `LocalAccountTokenFilterPolicy` (servers are not admin workstations)
|
||||||
|
- Defender exclusions for JetBrains (servers are not dev machines)
|
||||||
|
|
||||||
|
## WMI Filter
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| Name | Member Servers Only |
|
||||||
|
| Query | `SELECT * FROM Win32_OperatingSystem WHERE ProductType = 3` |
|
||||||
|
|
||||||
|
Defense-in-depth: ensures this GPO only applies to member servers (ProductType 3), not domain controllers (ProductType 2) or workstations (ProductType 1).
|
||||||
|
|
||||||
|
## Restricted Groups
|
||||||
|
|
||||||
|
| Local Group | Enforced Members |
|
||||||
|
|---|---|
|
||||||
|
| BUILTIN\Administrators | Domain Admins, MasterAdmins |
|
||||||
|
|
||||||
|
Any locally-added administrator accounts are removed on next GPO refresh.
|
||||||
|
|
||||||
|
## Security Policy Settings (GptTmpl.inf)
|
||||||
|
|
||||||
|
### System Access
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| EnableGuestAccount | 0 | Local guest account disabled |
|
||||||
|
|
||||||
|
### Event Audit
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| AuditSystemEvents | 3 | Success + Failure |
|
||||||
|
| AuditLogonEvents | 3 | Success + Failure |
|
||||||
|
| AuditObjectAccess | 3 | Success + Failure |
|
||||||
|
| AuditPrivilegeUse | 3 | Success + Failure |
|
||||||
|
| AuditPolicyChange | 3 | Success + Failure |
|
||||||
|
| AuditAccountManage | 3 | Success + Failure |
|
||||||
|
| AuditProcessTracking | 3 | Success + Failure |
|
||||||
|
| AuditDSAccess | 3 | Success + Failure |
|
||||||
|
| AuditAccountLogon | 3 | Success + Failure |
|
||||||
|
|
||||||
|
### Registry Values (Security Options)
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| InactivityTimeoutSecs | 900 | Auto-lock after 15 minutes |
|
||||||
|
| DontDisplayLastUserName | 1 | Don't show last user at login screen |
|
||||||
|
| DisableCAD | 0 | Require Ctrl+Alt+Del |
|
||||||
|
|
||||||
|
## Registry Settings (Administrative Templates)
|
||||||
|
|
||||||
|
### Autorun / Autoplay
|
||||||
|
|
||||||
|
| Key | ValueName | Value | Effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Policies\Explorer | NoDriveTypeAutoRun | 255 | Disable autorun on all drives |
|
||||||
|
| Policies\Explorer | NoAutorun | 1 | Disable autoplay |
|
||||||
|
|
||||||
|
### Windows Update
|
||||||
|
|
||||||
|
| Key | ValueName | Value | Effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| WindowsUpdate\AU | NoAutoUpdate | 0 | Enable automatic updates |
|
||||||
|
| WindowsUpdate\AU | AUOptions | 4 | Auto download + schedule install |
|
||||||
|
| WindowsUpdate\AU | ScheduledInstallDay | 1 | Sunday |
|
||||||
|
| WindowsUpdate\AU | ScheduledInstallTime | 3 | 3:00 AM |
|
||||||
|
|
||||||
|
### Logging & Auditing
|
||||||
|
|
||||||
|
| Key | ValueName | Value | Effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| PowerShell\ScriptBlockLogging | EnableScriptBlockLogging | 1 | Log all script blocks |
|
||||||
|
| PowerShell\Transcription | EnableTranscripting | 1 | Record full PS sessions |
|
||||||
|
| PowerShell\Transcription | OutputDirectory | C:\PSlogs\Transcripts | Transcript save location |
|
||||||
|
| PowerShell\Transcription | EnableInvocationHeader | 1 | Timestamp per command |
|
||||||
|
| PowerShell\ModuleLogging | EnableModuleLogging | 1 | Log all module activity |
|
||||||
|
| PowerShell\ModuleLogging\ModuleNames | * | * | All modules |
|
||||||
|
| System\Audit | ProcessCreationIncludeCmdLine_Enabled | 1 | Command line in Event 4688 |
|
||||||
|
|
||||||
|
### Event Log Sizes
|
||||||
|
|
||||||
|
| Log | Size | vs. Workstations-01 |
|
||||||
|
|---|---|---|
|
||||||
|
| Application | 64 MB | 2x |
|
||||||
|
| Security | 256 MB | ~1.3x |
|
||||||
|
| System | 64 MB | 2x |
|
||||||
|
| PowerShell | 64 MB | new |
|
||||||
|
|
||||||
|
### Remote Desktop
|
||||||
|
|
||||||
|
| Key | ValueName | Value | Effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Terminal Services | UserAuthentication | 1 | Require NLA |
|
||||||
357
gpo/servers-01/settings.ps1
Normal file
357
gpo/servers-01/settings.ps1
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
# Servers-01 -- Settings Declaration
|
||||||
|
# Linked to: OU=ExampleServers,DC=example,DC=internal
|
||||||
|
#
|
||||||
|
# Server hardening policy. Full audit coverage, PowerShell transcription,
|
||||||
|
# and operational baselines for domain-joined servers in the ExampleServers OU.
|
||||||
|
# All settings are Computer Configuration (HKLM).
|
||||||
|
|
||||||
|
@{
|
||||||
|
GPOName = 'Servers-01'
|
||||||
|
Description = 'Server hardening -- full audit, transcription, and operational baseline'
|
||||||
|
|
||||||
|
DisableUserConfiguration = $true
|
||||||
|
|
||||||
|
LinkTo = 'OU=ExampleServers,DC=example,DC=internal'
|
||||||
|
|
||||||
|
# Defense-in-depth: only apply to member servers (ProductType 3).
|
||||||
|
# ProductType 2 = domain controllers (have their own OU/GPO).
|
||||||
|
WMIFilter = @{
|
||||||
|
Name = 'Member Servers Only'
|
||||||
|
Description = 'Targets member server operating systems (ProductType = 3)'
|
||||||
|
Query = "SELECT * FROM Win32_OperatingSystem WHERE ProductType = 3"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lock down local Administrators to Domain Admins + MasterAdmins only.
|
||||||
|
# Any unauthorized additions are removed on next GPO refresh.
|
||||||
|
RestrictedGroups = @{
|
||||||
|
'BUILTIN\Administrators' = @{
|
||||||
|
Members = @('EXAMPLE\Domain Admins', 'EXAMPLE\MasterAdmins')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SecurityPolicy = @{
|
||||||
|
|
||||||
|
'System Access' = [ordered]@{
|
||||||
|
EnableGuestAccount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
'Event Audit' = [ordered]@{
|
||||||
|
# Full audit -- servers are high-value targets, log everything
|
||||||
|
AuditSystemEvents = 3 # Success + Failure
|
||||||
|
AuditLogonEvents = 3 # Success + Failure
|
||||||
|
AuditObjectAccess = 3 # Success + Failure
|
||||||
|
AuditPrivilegeUse = 3 # Success + Failure
|
||||||
|
AuditPolicyChange = 3 # Success + Failure
|
||||||
|
AuditAccountManage = 3 # Success + Failure
|
||||||
|
AuditProcessTracking = 3 # Success + Failure
|
||||||
|
AuditDSAccess = 3 # Success + Failure
|
||||||
|
AuditAccountLogon = 3 # Success + Failure
|
||||||
|
}
|
||||||
|
|
||||||
|
'Registry Values' = [ordered]@{
|
||||||
|
# Interactive logon: Machine inactivity limit -- 15 min (servers may have long console sessions)
|
||||||
|
'MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\InactivityTimeoutSecs' = '4,900'
|
||||||
|
|
||||||
|
# Interactive logon: Don't display last signed-in user
|
||||||
|
'MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\DontDisplayLastUserName' = '4,1'
|
||||||
|
|
||||||
|
# Interactive logon: Require CTRL+ALT+DEL
|
||||||
|
'MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\DisableCAD' = '4,0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Windows Firewall -- default-deny inbound, allow management traffic
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
FirewallProfiles = @{
|
||||||
|
Domain = @{ Enabled = $true; DefaultInboundAction = 'Block'; DefaultOutboundAction = 'Allow' }
|
||||||
|
Private = @{ Enabled = $true; DefaultInboundAction = 'Block'; DefaultOutboundAction = 'Allow' }
|
||||||
|
Public = @{ Enabled = $true; DefaultInboundAction = 'Block'; DefaultOutboundAction = 'Allow' }
|
||||||
|
}
|
||||||
|
|
||||||
|
FirewallRules = @(
|
||||||
|
# WinRM -- remote management (Invoke-Command, Enter-PSSession)
|
||||||
|
@{
|
||||||
|
DisplayName = 'Allow WinRM HTTP (Inbound)'
|
||||||
|
Direction = 'Inbound'
|
||||||
|
Action = 'Allow'
|
||||||
|
Protocol = 'TCP'
|
||||||
|
LocalPort = '5985'
|
||||||
|
Profile = 'Domain'
|
||||||
|
Description = 'Windows Remote Management (HTTP) for domain management'
|
||||||
|
}
|
||||||
|
|
||||||
|
# RDP -- remote console access (NLA required via registry setting above)
|
||||||
|
@{
|
||||||
|
DisplayName = 'Allow RDP (Inbound)'
|
||||||
|
Direction = 'Inbound'
|
||||||
|
Action = 'Allow'
|
||||||
|
Protocol = 'TCP'
|
||||||
|
LocalPort = '3389'
|
||||||
|
Profile = 'Domain'
|
||||||
|
Description = 'Remote Desktop Protocol for domain-authenticated sessions'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ICMP Echo -- network troubleshooting and monitoring
|
||||||
|
@{
|
||||||
|
DisplayName = 'Allow ICMP Echo Request (Inbound)'
|
||||||
|
Direction = 'Inbound'
|
||||||
|
Action = 'Allow'
|
||||||
|
Protocol = 'ICMPv4'
|
||||||
|
Profile = 'Domain'
|
||||||
|
Description = 'ICMP echo for ping-based health monitoring'
|
||||||
|
}
|
||||||
|
|
||||||
|
# SMB -- file sharing and administrative shares (C$, ADMIN$)
|
||||||
|
@{
|
||||||
|
DisplayName = 'Allow SMB (Inbound)'
|
||||||
|
Direction = 'Inbound'
|
||||||
|
Action = 'Allow'
|
||||||
|
Protocol = 'TCP'
|
||||||
|
LocalPort = '445'
|
||||||
|
Profile = 'Domain'
|
||||||
|
Description = 'Server Message Block for file sharing and admin shares'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# GPP Local Groups -- additive membership (does not replace full list)
|
||||||
|
# Use RestrictedGroups above for Administrators (exact membership).
|
||||||
|
# Use GPP Local Groups here for groups where we add members without
|
||||||
|
# wiping existing ones.
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
Preferences = @{
|
||||||
|
LocalUsersAndGroups = @(
|
||||||
|
@{
|
||||||
|
GroupName = 'Remote Desktop Users'
|
||||||
|
Action = 'Update'
|
||||||
|
DeleteAllUsers = $false
|
||||||
|
DeleteAllGroups = $false
|
||||||
|
Members = @(
|
||||||
|
@{ Name = 'EXAMPLE\MasterAdmins'; Action = 'ADD' }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Advanced Audit Policy -- granular subcategory-level auditing
|
||||||
|
# Overrides the legacy Event Audit above with 53 subcategories.
|
||||||
|
# When both are present, Advanced Audit takes precedence on clients.
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
AdvancedAuditPolicy = @{
|
||||||
|
# System
|
||||||
|
'Security State Change' = 'Success and Failure'
|
||||||
|
'Security System Extension' = 'Success and Failure'
|
||||||
|
'System Integrity' = 'Success and Failure'
|
||||||
|
|
||||||
|
# Logon/Logoff
|
||||||
|
'Logon' = 'Success and Failure'
|
||||||
|
'Logoff' = 'Success'
|
||||||
|
'Account Lockout' = 'Success'
|
||||||
|
'Special Logon' = 'Success and Failure'
|
||||||
|
'Other Logon/Logoff Events' = 'Success and Failure'
|
||||||
|
'Group Membership' = 'Success'
|
||||||
|
|
||||||
|
# Object Access
|
||||||
|
'File System' = 'Success and Failure'
|
||||||
|
'Registry' = 'Success and Failure'
|
||||||
|
'File Share' = 'Success and Failure'
|
||||||
|
'Detailed File Share' = 'Failure'
|
||||||
|
'SAM' = 'Success and Failure'
|
||||||
|
'Removable Storage' = 'Success and Failure'
|
||||||
|
'Filtering Platform Connection' = 'Failure'
|
||||||
|
|
||||||
|
# Privilege Use
|
||||||
|
'Sensitive Privilege Use' = 'Success and Failure'
|
||||||
|
|
||||||
|
# Detailed Tracking
|
||||||
|
'Process Creation' = 'Success and Failure'
|
||||||
|
'Process Termination' = 'Success'
|
||||||
|
'Plug and Play Events' = 'Success'
|
||||||
|
|
||||||
|
# Policy Change
|
||||||
|
'Audit Policy Change' = 'Success and Failure'
|
||||||
|
'Authentication Policy Change' = 'Success'
|
||||||
|
'MPSSVC Rule-Level Policy Change' = 'Success and Failure'
|
||||||
|
|
||||||
|
# Account Management
|
||||||
|
'User Account Management' = 'Success and Failure'
|
||||||
|
'Computer Account Management' = 'Success and Failure'
|
||||||
|
'Security Group Management' = 'Success and Failure'
|
||||||
|
'Other Account Management Events' = 'Success and Failure'
|
||||||
|
|
||||||
|
# Account Logon
|
||||||
|
'Credential Validation' = 'Success and Failure'
|
||||||
|
'Kerberos Authentication Service' = 'Success and Failure'
|
||||||
|
'Kerberos Service Ticket Operations' = 'Success and Failure'
|
||||||
|
}
|
||||||
|
|
||||||
|
RegistrySettings = @(
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Autorun / Autoplay
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# Disable autorun on all drive types (bitmask 0xFF = all)
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer'
|
||||||
|
ValueName = 'NoDriveTypeAutoRun'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 255
|
||||||
|
}
|
||||||
|
|
||||||
|
# Disable autoplay entirely
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer'
|
||||||
|
ValueName = 'NoAutorun'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Windows Update -- weekly Sunday 3 AM
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# Enable automatic updates
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU'
|
||||||
|
ValueName = 'NoAutoUpdate'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Auto download and schedule install
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU'
|
||||||
|
ValueName = 'AUOptions'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
# Schedule install day (0=Every day, 1=Sunday ... 7=Saturday)
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU'
|
||||||
|
ValueName = 'ScheduledInstallDay'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Schedule install time (0-23, 3 = 3:00 AM)
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU'
|
||||||
|
ValueName = 'ScheduledInstallTime'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Logging & Auditing -- full forensic capability
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# PowerShell script block logging
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging'
|
||||||
|
ValueName = 'EnableScriptBlockLogging'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# PowerShell transcription -- full session recording
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\Transcription'
|
||||||
|
ValueName = 'EnableTranscripting'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Transcription output directory
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\Transcription'
|
||||||
|
ValueName = 'OutputDirectory'
|
||||||
|
Type = 'String'
|
||||||
|
Value = 'C:\PSlogs\Transcripts'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Include invocation headers in transcripts (timestamps per command)
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\Transcription'
|
||||||
|
ValueName = 'EnableInvocationHeader'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# PowerShell module logging -- log all module activity
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging'
|
||||||
|
ValueName = 'EnableModuleLogging'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Log all modules (wildcard)
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging\ModuleNames'
|
||||||
|
ValueName = '*'
|
||||||
|
Type = 'String'
|
||||||
|
Value = '*'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Command line in process creation events (Event ID 4688)
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\Audit'
|
||||||
|
ValueName = 'ProcessCreationIncludeCmdLine_Enabled'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Event log sizes -- match AdminWorkstations (servers generate heavy event volume)
|
||||||
|
|
||||||
|
# Application log: 64 MB
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Application'
|
||||||
|
ValueName = 'MaxSize'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 65536
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security log: 256 MB
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Security'
|
||||||
|
ValueName = 'MaxSize'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 262144
|
||||||
|
}
|
||||||
|
|
||||||
|
# System log: 64 MB
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\EventLog\System'
|
||||||
|
ValueName = 'MaxSize'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 65536
|
||||||
|
}
|
||||||
|
|
||||||
|
# PowerShell Operational log: 64 MB
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Windows PowerShell'
|
||||||
|
ValueName = 'MaxSize'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 65536
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Remote Desktop
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# Require NLA for RDP
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows NT\Terminal Services'
|
||||||
|
ValueName = 'UserAuthentication'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
38
gpo/users-01/README.md
Normal file
38
gpo/users-01/README.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Users-01 GPO
|
||||||
|
|
||||||
|
**GUID:** Auto-created on first `Apply-GPOBaseline.ps1` run
|
||||||
|
**Linked to:** `OU=ExampleUsers,DC=example,DC=internal`
|
||||||
|
**Scope:** User Configuration (HKCU) -- Administrative Templates only
|
||||||
|
|
||||||
|
This GPO applies to all user accounts in the ExampleUsers OU. Settings follow the user to any domain-joined machine they log into.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
### Desktop Hardening
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| DisableRegistryTools | 1 | Blocks regedit.exe |
|
||||||
|
| DisableCMD | 2 | Blocks cmd.exe, allows batch files |
|
||||||
|
| NoRun | 1 | Removes Run from Start Menu |
|
||||||
|
| NoChangingWallPaper | 1 | Prevents changing desktop wallpaper |
|
||||||
|
| NoAddRemovePrograms | 1 | Hides Programs & Features in Control Panel |
|
||||||
|
| NoAddPrinter | 1 | Prevents adding printers |
|
||||||
|
|
||||||
|
### UX Standardization
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| Wallpaper | `C:\Windows\Web\Wallpaper\Windows\img0.jpg` | Default Windows wallpaper (replace with corporate UNC path when ready) |
|
||||||
|
| WallpaperStyle | 10 | Fill mode |
|
||||||
|
| SearchboxTaskbarMode | 0 | Hides Search box on taskbar |
|
||||||
|
| ShowTaskViewButton | 0 | Hides Task View button |
|
||||||
|
| TurnOffWindowsCopilot | 1 | Disables Windows Copilot |
|
||||||
|
| TaskbarDa | 0 | Hides Widgets |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- No SecurityPolicy (GptTmpl.inf) settings -- user rights, account policies, and audit settings are Computer Configuration only
|
||||||
|
- All 12 settings are registry-based, applied via `Set-GPRegistryValue`
|
||||||
|
- Wallpaper currently points to the built-in Windows image; replace with a UNC path (e.g., `\\example.internal\NETLOGON\wallpaper.jpg`) when a corporate wallpaper is ready
|
||||||
|
- Taskbar settings (Widgets, Copilot) are Windows 11 / Server 2025 specific -- no-ops on older OS
|
||||||
133
gpo/users-01/settings.ps1
Normal file
133
gpo/users-01/settings.ps1
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
# Users-01 -- Settings Declaration
|
||||||
|
# Linked to: OU=ExampleUsers,DC=example,DC=internal
|
||||||
|
#
|
||||||
|
# This GPO targets user configuration for the ExampleUsers OU.
|
||||||
|
# All settings are User Configuration (HKCU) -- Administrative Templates.
|
||||||
|
|
||||||
|
@{
|
||||||
|
GPOName = 'Users-01'
|
||||||
|
Description = 'Standard user desktop hardening and UX standardization'
|
||||||
|
|
||||||
|
DisableComputerConfiguration = $true
|
||||||
|
|
||||||
|
LinkTo = 'OU=ExampleUsers,DC=example,DC=internal'
|
||||||
|
|
||||||
|
# Deny Apply for admin groups -- DelegatedAdmins sit in ExampleUsers but should not
|
||||||
|
# receive desktop restrictions (they need regedit, cmd, etc. for sysadmin work).
|
||||||
|
# MasterAdmins are in ExampleAdmins OU so they never receive this GPO anyway.
|
||||||
|
SecurityFiltering = @{
|
||||||
|
DenyApply = @('DelegatedAdmins')
|
||||||
|
}
|
||||||
|
|
||||||
|
# No security policy settings -- user rights, account policies, etc. are Computer Configuration only
|
||||||
|
SecurityPolicy = @{}
|
||||||
|
|
||||||
|
RegistrySettings = @(
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Desktop Hardening
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# Prevent access to registry editing tools
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'
|
||||||
|
ValueName = 'DisableRegistryTools'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prevent access to command prompt (2 = disable cmd.exe but allow batch files)
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Policies\Microsoft\Windows\System'
|
||||||
|
ValueName = 'DisableCMD'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove Run from Start Menu
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer'
|
||||||
|
ValueName = 'NoRun'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prevent changing desktop wallpaper
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\ActiveDesktop'
|
||||||
|
ValueName = 'NoChangingWallPaper'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove Add/Remove Programs from Control Panel
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Uninstall'
|
||||||
|
ValueName = 'NoAddRemovePrograms'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prevent adding printers
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer'
|
||||||
|
ValueName = 'NoAddPrinter'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# UX Standardization
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# Set default desktop wallpaper (built-in Windows wallpaper, exists on all machines)
|
||||||
|
# Replace with a corporate wallpaper on a UNC share when ready
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'
|
||||||
|
ValueName = 'Wallpaper'
|
||||||
|
Type = 'String'
|
||||||
|
Value = 'C:\Windows\Web\Wallpaper\Windows\img0.jpg'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wallpaper style: Fill
|
||||||
|
# 0=Center, 2=Stretch, 6=Fit, 10=Fill, 22=Span
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'
|
||||||
|
ValueName = 'WallpaperStyle'
|
||||||
|
Type = 'String'
|
||||||
|
Value = '10'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hide Search box on taskbar (0=Hidden, 1=Icon, 2=Full box)
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Search'
|
||||||
|
ValueName = 'SearchboxTaskbarMode'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hide Task View button on taskbar
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced'
|
||||||
|
ValueName = 'ShowTaskViewButton'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Disable Windows Copilot
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Policies\Microsoft\Windows\WindowsCopilot'
|
||||||
|
ValueName = 'TurnOffWindowsCopilot'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hide Widgets on taskbar
|
||||||
|
@{
|
||||||
|
Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced'
|
||||||
|
ValueName = 'TaskbarDa'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
96
gpo/workstations-01/README.md
Normal file
96
gpo/workstations-01/README.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# Workstations-01 GPO
|
||||||
|
|
||||||
|
**GUID:** Auto-created on first `Apply-GPOBaseline.ps1` run
|
||||||
|
**Linked to:** `OU=ExampleWorkstations,DC=example,DC=internal`
|
||||||
|
**Scope:** Computer Configuration (HKLM) -- Security Policy + Administrative Templates
|
||||||
|
|
||||||
|
This GPO applies to all computer objects in the ExampleWorkstations OU. It uses both SecurityPolicy (GptTmpl.inf) and RegistrySettings (Set-GPRegistryValue).
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
### Security Policy (GptTmpl.inf)
|
||||||
|
|
||||||
|
#### System Access
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| EnableGuestAccount | 0 | Disables the local guest account |
|
||||||
|
|
||||||
|
#### Event Audit
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| AuditSystemEvents | 1 | Success |
|
||||||
|
| AuditLogonEvents | 3 | Success + Failure |
|
||||||
|
| AuditObjectAccess | 2 | Failure |
|
||||||
|
| AuditPrivilegeUse | 2 | Failure |
|
||||||
|
| AuditPolicyChange | 1 | Success |
|
||||||
|
| AuditAccountManage | 3 | Success + Failure |
|
||||||
|
| AuditProcessTracking | 0 | No auditing |
|
||||||
|
| AuditDSAccess | 0 | No auditing (irrelevant for workstations) |
|
||||||
|
| AuditAccountLogon | 3 | Success + Failure |
|
||||||
|
|
||||||
|
#### Security Options (Registry Values in GptTmpl.inf)
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| InactivityTimeoutSecs | 900 | Lock screen after 15 minutes idle |
|
||||||
|
| DontDisplayLastUserName | 1 | Login screen does not reveal usernames |
|
||||||
|
| DisableCAD | 0 | Ctrl+Alt+Del required at login |
|
||||||
|
|
||||||
|
### Administrative Templates (Registry-based)
|
||||||
|
|
||||||
|
#### Autorun / Autoplay
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| NoDriveTypeAutoRun | 255 | Disable autorun on all drive types |
|
||||||
|
| NoAutorun | 1 | Disable autoplay entirely |
|
||||||
|
|
||||||
|
#### Windows Update
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| NoAutoUpdate | 0 | Automatic updates enabled |
|
||||||
|
| AUOptions | 4 | Auto download + scheduled install |
|
||||||
|
| ScheduledInstallDay | 0 | Every day |
|
||||||
|
| ScheduledInstallTime | 3 | 3:00 AM |
|
||||||
|
|
||||||
|
#### Logging & Auditing
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| EnableScriptBlockLogging | 1 | PowerShell script block logging enabled |
|
||||||
|
| Application MaxSize | 32768 KB | 32 MB application event log |
|
||||||
|
| Security MaxSize | 196608 KB | 192 MB security event log |
|
||||||
|
| System MaxSize | 32768 KB | 32 MB system event log |
|
||||||
|
|
||||||
|
#### Remote Desktop
|
||||||
|
|
||||||
|
| Setting | Value | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| UserAuthentication | 1 | Network Level Authentication required for RDP |
|
||||||
|
|
||||||
|
## WMI Filter
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| Name | Workstations Only |
|
||||||
|
| Query | `SELECT * FROM Win32_OperatingSystem WHERE ProductType = 1` |
|
||||||
|
|
||||||
|
Defense-in-depth: ensures this GPO only applies to workstation operating systems, even if a server object lands in the ExampleWorkstations OU by mistake.
|
||||||
|
|
||||||
|
## Restricted Groups
|
||||||
|
|
||||||
|
| Local Group | Enforced Members |
|
||||||
|
|---|---|
|
||||||
|
| BUILTIN\Administrators | Domain Admins, MasterAdmins |
|
||||||
|
|
||||||
|
Any locally-added administrator accounts are removed on next GPO refresh. This prevents local admin creep on workstations.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- First GPO in this repo to use both SecurityPolicy and RegistrySettings together
|
||||||
|
- Audit policy uses legacy categories (not Advanced Audit Policy Configuration subcategories)
|
||||||
|
- Event log sizes are generous -- 192 MB security log supports forensic investigation
|
||||||
|
- Windows Update schedule assumes workstations are powered on overnight or use wake timers
|
||||||
288
gpo/workstations-01/settings.ps1
Normal file
288
gpo/workstations-01/settings.ps1
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
# Workstations-01 -- Settings Declaration
|
||||||
|
# Linked to: OU=ExampleWorkstations,DC=example,DC=internal
|
||||||
|
#
|
||||||
|
# This GPO targets computer configuration for the ExampleWorkstations OU.
|
||||||
|
# All settings are Computer Configuration (HKLM) -- both Security Policy and Administrative Templates.
|
||||||
|
|
||||||
|
@{
|
||||||
|
GPOName = 'Workstations-01'
|
||||||
|
Description = 'Workstation hardening -- security, updates, logging, and remote desktop'
|
||||||
|
|
||||||
|
DisableUserConfiguration = $true
|
||||||
|
|
||||||
|
LinkTo = 'OU=ExampleWorkstations,DC=example,DC=internal'
|
||||||
|
|
||||||
|
# Defense-in-depth: only apply to workstation OS (ProductType 1).
|
||||||
|
# Prevents misapplication if a server object lands in the wrong OU.
|
||||||
|
WMIFilter = @{
|
||||||
|
Name = 'Workstations Only'
|
||||||
|
Description = 'Targets workstation operating systems (ProductType = 1)'
|
||||||
|
Query = "SELECT * FROM Win32_OperatingSystem WHERE ProductType = 1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lock down local Administrators to Domain Admins + MasterAdmins only.
|
||||||
|
# Any unauthorized additions are removed on next GPO refresh.
|
||||||
|
RestrictedGroups = @{
|
||||||
|
'BUILTIN\Administrators' = @{
|
||||||
|
Members = @('EXAMPLE\Domain Admins', 'EXAMPLE\MasterAdmins')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SecurityPolicy = @{
|
||||||
|
|
||||||
|
'System Access' = [ordered]@{
|
||||||
|
# Disable local guest account
|
||||||
|
EnableGuestAccount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
'Event Audit' = [ordered]@{
|
||||||
|
# Audit values: 0=None, 1=Success, 2=Failure, 3=Success+Failure
|
||||||
|
AuditSystemEvents = 1 # Success
|
||||||
|
AuditLogonEvents = 3 # Success + Failure
|
||||||
|
AuditObjectAccess = 2 # Failure
|
||||||
|
AuditPrivilegeUse = 2 # Failure
|
||||||
|
AuditPolicyChange = 1 # Success
|
||||||
|
AuditAccountManage = 3 # Success + Failure
|
||||||
|
AuditProcessTracking = 0 # No auditing
|
||||||
|
AuditDSAccess = 0 # No auditing (irrelevant for workstations)
|
||||||
|
AuditAccountLogon = 3 # Success + Failure
|
||||||
|
}
|
||||||
|
|
||||||
|
'Registry Values' = [ordered]@{
|
||||||
|
# Interactive logon: Machine inactivity limit (seconds) - lock after 15 min
|
||||||
|
'MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\InactivityTimeoutSecs' = '4,900'
|
||||||
|
|
||||||
|
# Interactive logon: Don't display last signed-in user
|
||||||
|
'MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\DontDisplayLastUserName' = '4,1'
|
||||||
|
|
||||||
|
# Interactive logon: Do not require CTRL+ALT+DEL = Disabled (i.e. require it)
|
||||||
|
'MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\DisableCAD' = '4,0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Windows Firewall -- default-deny inbound, minimal exceptions
|
||||||
|
# Standard workstations should not host inbound services.
|
||||||
|
# Only ICMP for troubleshooting.
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
FirewallProfiles = @{
|
||||||
|
Domain = @{ Enabled = $true; DefaultInboundAction = 'Block'; DefaultOutboundAction = 'Allow' }
|
||||||
|
Private = @{ Enabled = $true; DefaultInboundAction = 'Block'; DefaultOutboundAction = 'Allow' }
|
||||||
|
Public = @{ Enabled = $true; DefaultInboundAction = 'Block'; DefaultOutboundAction = 'Allow' }
|
||||||
|
}
|
||||||
|
|
||||||
|
FirewallRules = @(
|
||||||
|
# ICMP Echo -- network troubleshooting
|
||||||
|
@{
|
||||||
|
DisplayName = 'Allow ICMP Echo Request (Inbound)'
|
||||||
|
Direction = 'Inbound'
|
||||||
|
Action = 'Allow'
|
||||||
|
Protocol = 'ICMPv4'
|
||||||
|
Profile = 'Domain'
|
||||||
|
Description = 'ICMP echo for ping-based connectivity testing'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# AppLocker -- audit mode (logs violations, does not block)
|
||||||
|
# Standard baseline: allow Microsoft-signed, allow from Windows
|
||||||
|
# and Program Files, allow admins unrestricted. Deploy in AuditOnly
|
||||||
|
# first to identify any legitimate apps that would be blocked.
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
AppLockerPolicy = @{
|
||||||
|
Exe = @{
|
||||||
|
EnforcementMode = 'AuditOnly'
|
||||||
|
Rules = @(
|
||||||
|
@{
|
||||||
|
Type = 'Publisher'
|
||||||
|
Name = 'Allow Microsoft-signed executables'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'Everyone'
|
||||||
|
Publisher = 'O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US'
|
||||||
|
Product = '*'
|
||||||
|
Binary = '*'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Type = 'Path'
|
||||||
|
Name = 'Allow executables in Program Files'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'Everyone'
|
||||||
|
Path = '%PROGRAMFILES%\*'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Type = 'Path'
|
||||||
|
Name = 'Allow executables in Windows directory'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'Everyone'
|
||||||
|
Path = '%WINDIR%\*'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Type = 'Path'
|
||||||
|
Name = 'Allow administrators unrestricted'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'BUILTIN\Administrators'
|
||||||
|
Path = '*'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Msi = @{
|
||||||
|
EnforcementMode = 'AuditOnly'
|
||||||
|
Rules = @(
|
||||||
|
@{
|
||||||
|
Type = 'Publisher'
|
||||||
|
Name = 'Allow Microsoft-signed installers'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'Everyone'
|
||||||
|
Publisher = 'O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US'
|
||||||
|
Product = '*'
|
||||||
|
Binary = '*'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Type = 'Path'
|
||||||
|
Name = 'Allow administrators unrestricted'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'BUILTIN\Administrators'
|
||||||
|
Path = '*'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Script = @{
|
||||||
|
EnforcementMode = 'AuditOnly'
|
||||||
|
Rules = @(
|
||||||
|
@{
|
||||||
|
Type = 'Path'
|
||||||
|
Name = 'Allow scripts in Windows directory'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'Everyone'
|
||||||
|
Path = '%WINDIR%\*'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Type = 'Path'
|
||||||
|
Name = 'Allow scripts in Program Files'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'Everyone'
|
||||||
|
Path = '%PROGRAMFILES%\*'
|
||||||
|
}
|
||||||
|
@{
|
||||||
|
Type = 'Path'
|
||||||
|
Name = 'Allow administrators unrestricted'
|
||||||
|
Action = 'Allow'
|
||||||
|
User = 'BUILTIN\Administrators'
|
||||||
|
Path = '*'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RegistrySettings = @(
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Autorun / Autoplay
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# Disable autorun on all drive types (bitmask 0xFF = all)
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer'
|
||||||
|
ValueName = 'NoDriveTypeAutoRun'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 255
|
||||||
|
}
|
||||||
|
|
||||||
|
# Disable autoplay entirely
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer'
|
||||||
|
ValueName = 'NoAutorun'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Windows Update
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# Enable automatic updates
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU'
|
||||||
|
ValueName = 'NoAutoUpdate'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Auto download and schedule install
|
||||||
|
# 2=Notify, 3=Auto download + notify, 4=Auto download + schedule, 5=Local admin chooses
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU'
|
||||||
|
ValueName = 'AUOptions'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
# Schedule install day (0=Every day, 1=Sunday ... 7=Saturday)
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU'
|
||||||
|
ValueName = 'ScheduledInstallDay'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Schedule install time (0-23, 3 = 3:00 AM)
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU'
|
||||||
|
ValueName = 'ScheduledInstallTime'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Logging & Auditing
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# Enable PowerShell script block logging
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging'
|
||||||
|
ValueName = 'EnableScriptBlockLogging'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Event log max sizes (in KB)
|
||||||
|
|
||||||
|
# Application log: 32 MB
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Application'
|
||||||
|
ValueName = 'MaxSize'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 32768
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security log: 192 MB
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\EventLog\Security'
|
||||||
|
ValueName = 'MaxSize'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 196608
|
||||||
|
}
|
||||||
|
|
||||||
|
# System log: 32 MB
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows\EventLog\System'
|
||||||
|
ValueName = 'MaxSize'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 32768
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================
|
||||||
|
# Remote Desktop
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
# Require Network Level Authentication for RDP connections
|
||||||
|
@{
|
||||||
|
Key = 'HKLM\Software\Policies\Microsoft\Windows NT\Terminal Services'
|
||||||
|
ValueName = 'UserAuthentication'
|
||||||
|
Type = 'DWord'
|
||||||
|
Value = 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user