Projects STRLCPY GOAD Commits 420b54bd
🤬
  • ■ ■ ■ ■ ■
    ad/sevenkingdoms.local/data/config.json
    skipped 154 lines
    155 155   "netbios_name": "ESSOS",
    156 156   "ca_server": "Braavos",
    157 157   "trust" : "sevenkingdoms.local",
    158  - "organisation_units" : {},
     158 + "organisation_units" : {
     159 + "Workstations" : { "path" : "DC=essos,DC=local" }
     160 + },
    159 161   "groups" : {
    160 162   "universal" : {},
    161 163   "global" : {
    skipped 85 lines
    247 249   "domain_password" : "NgtI75cKV+Pu",
    248 250   "netbios_name": "NORTH",
    249 251   "trust" : "",
    250  - "organisation_units" : {},
     252 + "organisation_units" : {
     253 + "Workstations" : { "path" : "DC=north,DC=sevenkingdoms,DC=local" }
     254 + },
    251 255   "groups" : {
    252 256   "universal" : {},
    253 257   "global" : {
    skipped 300 lines
  • ■ ■ ■ ■ ■ ■
    ansible/laps.yml
     1 +---
     2 +# Load datas
     3 +- import_playbook: data.yml
     4 + vars:
     5 + data_path: "../ad/{{domain_name}}/data/"
     6 + tags: 'data'
     7 + 
     8 +- name: configure laps
     9 + hosts: dc02
     10 + roles:
     11 + - { role: 'laps', move_computer: dc02 }
     12 + 
     13 +- name: configure laps
     14 + hosts: dc03
     15 + roles:
     16 + - { role: 'laps', move_computer: dc03 }
     17 + 
     18 +- name: configure laps
     19 + hosts: dc01, dc02, dc03
     20 + roles:
     21 + - { role: 'laps', prep_servers: True }
     22 + 
     23 +- name: configure laps
     24 + hosts: dc02
     25 + roles:
     26 + - { role: 'laps', apply_dacl: dc02 }
     27 + 
     28 +- name: configure laps
     29 + hosts: dc03
     30 + roles:
     31 + - { role: 'laps', apply_dacl: dc03 }
     32 + 
     33 +- name: configure laps
     34 + hosts: dc01, dc02, dc03
     35 + roles:
     36 + - { role: 'laps', create_gpo: True }
     37 + 
     38 +- name: configure laps
     39 + hosts: dc02
     40 + roles:
     41 + - { role: 'laps', gpo_linked: dc02 }
     42 + 
     43 +- name: configure laps
     44 + hosts: dc03
     45 + roles:
     46 + - { role: 'laps', gpo_linked: dc03 }
     47 + 
     48 +- name: configure laps
     49 + hosts: srv02, srv03
     50 + roles:
     51 + - { role: 'laps', install_servers: True }
     52 + 
     53 +- name: configure laps
     54 + hosts: dc02
     55 + roles:
     56 + - { role: 'laps', test_deployment: dc02 }
     57 + 
     58 +- name: configure laps
     59 + hosts: dc03
     60 + roles:
     61 + - { role: 'laps', test_deployment: dc03 }
     62 + 
  • ■ ■ ■ ■ ■ ■
    ansible/main.yml
    skipped 15 lines
    16 16  - import_playbook: ad-trusts.yml
    17 17  # import the ad datas : users/groups...
    18 18  - import_playbook: ad-data.yml
     19 +# install LAPS
     20 +- import_playbook: laps.yml
    19 21  ## MSSQL + IIS ----------
    20 22  # configure servers vulns (done in the midle of ad install to let time before install relations and acl)
    21 23  - import_playbook: servers.yml
    skipped 13 lines
  • ■ ■ ■ ■ ■ ■
    ansible/roles/laps/defaults/main.yml
     1 +---
     2 +move_computer: False
     3 +prep_servers: False
     4 +apply_dacl: False
     5 +create_gpo: False
     6 +gpo_linked: False
     7 +install_servers: False
     8 +test_deployment: False
  • ■ ■ ■ ■ ■ ■
    ansible/roles/laps/files/comment.cmtx
     1 +<?xml version='1.0' encoding='utf-8'?>
     2 +<policyComments xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.0" schemaVersion="1.0" xmlns="http://www.microsoft.com/GroupPolicy/CommentDefinitions">
     3 + <policyNamespaces>
     4 + <using prefix="ns0" namespace="FullArmor.Policies.C9E1D975_EA58_48C3_958E_3BC214D89A2E"></using>
     5 + </policyNamespaces>
     6 + <comments>
     7 + <admTemplate></admTemplate>
     8 + </comments>
     9 + <resources minRequiredRevision="1.0">
     10 + <stringTable></stringTable>
     11 + </resources>
     12 +</policyComments>
  • ■ ■ ■ ■ ■ ■
    ansible/roles/laps/library/win_ad_dacl.ps1
     1 +#!powershell
     2 + 
     3 +# Copyright: (c) 2018, Jordan Borean
     4 +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
     5 + 
     6 +#Requires -Module Ansible.ModuleUtils.Legacy
     7 +#Requires -Module Ansible.ModuleUtils.SID
     8 + 
     9 +$ErrorActionPreference = "Stop"
     10 + 
     11 +$params = Parse-Args -arguments $args -supports_check_mode $true
     12 +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
     13 + 
     14 +$path = Get-AnsibleParam -obj $params -name "path" -type "str" -failifempty $true
     15 +$aces = Get-AnsibleParam -obj $params -name "aces" -type "list" -failifempty $true
     16 +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "present"
     17 + 
     18 +$result = @{
     19 + changed = $false
     20 +}
     21 + 
     22 +Import-Module -Name ActiveDirectory
     23 + 
     24 +$common_ad_parameters = @{}
     25 + 
     26 +Function ConvertTo-SchemaGuid {
     27 + param(
     28 + [Parameter(Mandatory=$true)]$Value,
     29 + [Parameter(Mandatory=$true)][String]$Name,
     30 + [Parameter(Mandatory=$true)][Int32]$Entry,
     31 + [Hashtable]$CommonParameters
     32 + )
     33 + 
     34 + if ($null -eq $Value.$Name) {
     35 + return [System.Guid]::Empty
     36 + }
     37 + $raw_value = $Value.$Name
     38 + 
     39 + try {
     40 + $guid = [System.Guid]::Parse($raw_value)
     41 + return $guid
     42 + } catch [System.FormatException] {} # not a GUID, we try and convert by scanning AD
     43 + 
     44 + $root_schema = (Get-ADRootDSE @CommonParameters).schemaNamingContext
     45 + $id_object = Get-ADObject -Filter { Name -eq $raw_value } -SearchBase $root_schema -Property schemaIDGUID @CommonParameters
     46 + if ($null -eq $id_object) {
     47 + Fail-Json -obj $result -message "Failed to convert ace entry option $Entry $Name to object guid '$($raw_value)'"
     48 + } else {
     49 + $schema_guid = New-Object -TypeName System.Guid -ArgumentList @(,$id_object.schemaIDGUID)
     50 + return $schema_guid
     51 + }
     52 +}
     53 + 
     54 +# Convert the input ace entries to a format we can compare against the real ACEs
     55 +$valid_access_rights = [System.Enum]::GetNames([System.DirectoryServices.ActiveDirectoryRights])
     56 +$valid_inheritance_types = [System.Enum]::GetNames([System.DirectoryServices.ActiveDirectorySecurityInheritance])
     57 + 
     58 +$raw_aces = [System.Collections.ArrayList]@()
     59 +foreach ($ace in $aces) {
     60 + $identity_sid_str = Convert-ToSID -account_name $ace.account
     61 + 
     62 + if ($null -eq $ace.rights) {
     63 + $msg = "Found undefined ace entry index $($raw_aces.Count) rights. Valid options $($valid_access_rights -join ", ")"
     64 + Fail-Json -obj $result -message $msg
     65 + }
     66 + $ace_rights = $ace.rights
     67 + if ($ace_rights -isnot [Array]) {
     68 + $ace_rights = $ace_rights.ToString().Split(",").Trim()
     69 + }
     70 + 
     71 + foreach ($ace_right in $ace_rights) {
     72 + if ($ace_right -notin $valid_access_rights) {
     73 + $msg = "Invalid value for ace entry index $(raw_aces.Count) rights, '$ace_right'. Valid options $($valid_access_rights -join ", ")"
     74 + Fail-Json -obj $result -message $msg
     75 + }
     76 + }
     77 + $ace_rights = [System.Enum]::Parse([System.DirectoryServices.ActiveDirectoryRights], $ace_rights -join ", ", $true)
     78 + 
     79 + $access = switch($ace.access) {
     80 + "allow" { [System.Security.AccessControl.AccessControlType]::Allow }
     81 + "deny" { [System.Security.AccessControl.AccessControlType]::Deny }
     82 + default { Fail-Json -obj $result -message "Invalid value for ace entry index $($raw_aces.Count) access, '$access'. Valid options allow, deny" }
     83 + }
     84 + 
     85 + if ($ace.inheritance_type -notin $valid_inheritance_types) {
     86 + $msg = "Invalid value for ace entry index $($raw_aces.Count) inheritance_type, '$($ace.inheritance_type)'. Valid options $($valid_inheritance_types -join ", ")"
     87 + Fail-Json -obj $result -message $msg
     88 + }
     89 + $inheritance_type = [System.DirectoryServices.ActiveDirectorySecurityInheritance]$ace.inheritance_type
     90 + 
     91 + $object_type = ConvertTo-SchemaGuid -Value $ace `
     92 + -Name "object_type" -Entry $raw_aces.Length `
     93 + -CommonParameters $common_ad_parameters
     94 + 
     95 + $inherited_object_type = ConvertTo-SchemaGuid -Value $ace `
     96 + -Name "inherited_object_type" -Entry $raw_aces.Count `
     97 + -CommonParameters $common_ad_parameters
     98 + 
     99 + $raw_ace = New-Object -TypeName System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList @(
     100 + (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $identity_sid_str),
     101 + $ace_rights,
     102 + $access,
     103 + $object_type,
     104 + $inheritance_type
     105 + $inherited_object_type
     106 + )
     107 + $raw_aces.Add($raw_ace) > $null
     108 +}
     109 + 
     110 +# Now get the actual DACL for the AD object specified
     111 +$ad_object = Get-ADObject -Identity $path -Properties nTSecurityDescriptor @common_ad_parameters
     112 + 
     113 +$actual_sd = $ad_object.nTSecurityDescriptor
     114 +$actual_dacl = $actual_sd.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])
     115 +$comparison_props = @(
     116 + "ActiveDirectoryRights",
     117 + "InheritanceType",
     118 + "ObjectType",
     119 + "InheritedObjectType",
     120 + "AccessControlType",
     121 + "IdentityReference"
     122 +)
     123 + 
     124 +foreach ($raw_ace in $raw_aces) {
     125 + $found_ace = $null
     126 + 
     127 + foreach ($actual_ace in $actual_dacl) {
     128 + $mismatched = $false
     129 + foreach ($comparison_prop in $comparison_props) {
     130 + if ($actual_ace.$comparison_prop -ne $raw_ace.$comparison_prop) {
     131 + $mismatched = $true
     132 + break
     133 + }
     134 + }
     135 + if (-not $mismatched) {
     136 + $found_ace = $actual_ace
     137 + break
     138 + }
     139 + }
     140 + 
     141 + if ($state -eq "absent" -and $null -ne $found_ace) {
     142 + $ad_object.nTSecurityDescriptor.RemoveAccessRuleSpecific($found_ace)
     143 + $result.changed = $true
     144 + } elseif ($state -eq "present" -and $null -eq $found_ace) {
     145 + $ad_object.nTSecurityDescriptor.AddAccessRule($raw_ace)
     146 + $result.changed = $true
     147 + }
     148 +}
     149 + 
     150 +if ($result.changed -eq $true) {
     151 + $replacements = @{
     152 + nTSecurityDescriptor = $ad_object.nTSecurityDescriptor
     153 + }
     154 + Set-ADObject -Identity $ad_object.ObjectGUID -Replace $replacements -WhatIf:$check_mode @common_ad_parameters
     155 +}
     156 + 
     157 +Exit-Json -obj $result
  • ■ ■ ■ ■ ■ ■
    ansible/roles/laps/library/win_ad_object.ps1
     1 +#!powershell
     2 + 
     3 +# Copyright: (c) 2018, Jordan Borean
     4 +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
     5 + 
     6 +#Requires -Module Ansible.ModuleUtils.Legacy
     7 + 
     8 +$ErrorActionPreference = "Stop"
     9 + 
     10 +$params = Parse-Args -arguments $args -supports_check_mode $true
     11 +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
     12 + 
     13 +$attributes = Get-AnsibleParam -obj $params -name "attributes"
     14 +$context = Get-AnsibleParam -obj $params -name "context" -type "str" -default "default" -validateset "configuration", "default", "root_domain", "schema"
     15 +# $domain_server = Get-AnsibleParam -obj $params -name "domain_server" -type "str"
     16 +# $domain_username = Get-AnsibleParam -obj $params -name "domain_username" -type "str"
     17 +# $domain_password = Get-AnsibleParam -obj $params -name "domain_password" -type "str" -failifempty ($null -ne $domain_username)
     18 +$may_contain = Get-AnsibleParam -obj $params -name "may_contain" -type "list"
     19 +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
     20 +$type = Get-AnsibleParam -obj $params -name "type" -type "str" -default "attribute" -validateset "attribute", "class"
     21 +$update_schema = Get-AnsibleParam -obj $params -name "update_schema" -type "bool" -default $false
     22 + 
     23 +$result = @{
     24 + changed = $false
     25 +}
     26 + 
     27 +Import-Module -Name ActiveDirectory
     28 + 
     29 +$common_params = @{}
     30 +# if ($null -ne $domain_server) {
     31 +# $common_params.Server = $domain_server
     32 +# }
     33 +# if ($null -ne $domain_username) {
     34 +# $sec_pass = ConvertTo-SecureString -String $domain_password -AsPlainText -Force
     35 +# $common_params.Credential = (New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $domain_username, $sec_pass)
     36 +# }
     37 + 
     38 +$root_dse = Get-ADRootDSE -Properties schemaNamingContext, rootDomainNamingContext @common_params
     39 +$context = switch($context) {
     40 + "configuration" { $root_dse.configurationNamingContext }
     41 + "default" { $root_dse.defaultNamingContext }
     42 + "root_domain" { $root_dse.rootDomainNamingContext }
     43 + "schema" { $root_dse.schemaNamingContext }
     44 +}
     45 + 
     46 +try {
     47 + $search_filter = { DistinguishedName -eq $name -or ldapDisplayName -eq $name }
     48 + $existing_obj = Get-ADObject -SearchBase $context -Filter $search_filter -Properties * @common_params
     49 +} catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
     50 + $existing_obj = $null
     51 +}
     52 + 
     53 +if ($null -eq $existing_obj) {
     54 + $ldap_type = switch ($type) {
     55 + "attribute" { "attributeSchema" }
     56 + "class" { "classSchema" }
     57 + }
     58 + New-ADObject -Name $name -Path $context -OtherAttributes $attributes `
     59 + -Type $ldap_type -WhatIf:$check_mode @common_params
     60 + $result.changed = $true
     61 +} elseif ($null -ne $attributes) {
     62 + # Compare the input attributes with the existing attributes and change if required
     63 + $replacements = @{}
     64 + $additions = @{}
     65 + $clear = [System.Collections.Generic.List`1[String]]@()
     66 + 
     67 + foreach ($attribute_entry in $attributes.GetEnumerator()) {
     68 + $attribute_key = $attribute_entry.Key
     69 + $attribute_value = $attribute_entry.Value
     70 + 
     71 + $existing_value = $existing_obj.$attribute_key
     72 + if ($null -eq $attribute_value -and $null -ne $existing_value) {
     73 + $clear.Add($attribute_key) > $null
     74 + } elseif ($null -ne $attribute_value -and $null -eq $existing_value) {
     75 + $additions.$attribute_key = $attribute_value
     76 + } elseif ($attribute_value -ne $existing_value) {
     77 + $replacements.$attribute_key = $attribute_value
     78 + }
     79 + }
     80 + 
     81 + if ($replacements.Count -gt 0 -or $additions.Count -gt 0 -or $clear.Count -gt 0) {
     82 + $set_params = $common_params.Clone()
     83 + if ($replacements.Count -gt 0) {
     84 + $set_params.Replace = $replacements
     85 + }
     86 + if ($additions.Count -gt 0) {
     87 + $set_params.Add = $additions
     88 + }
     89 + if ($clear.Count -gt 0) {
     90 + $set_params.Clear = $clear.ToArray()
     91 + }
     92 + 
     93 + Set-ADObject -Identity $existing_obj.ObjectGuid @set_params
     94 + $result.changed = $true
     95 + }
     96 +}
     97 + 
     98 +# Now set the mayContain attributes, we do a last check on existing_obj in case we are in check mode
     99 +if ($null -ne $may_contain -and $null -ne $existing_obj) {
     100 + foreach ($may_contain_entry in $may_contain) {
     101 + if (-not $existing_obj.mayContain.Contains($may_contain_entry)) {
     102 + Set-ADObject -Identity $existing_obj.ObjectGuid -Add @{ mayContain = $may_contain_entry } @common_params > $null
     103 + $result.changed = $true
     104 + }
     105 + }
     106 +}
     107 + 
     108 +# Reload the schema cache if a change occurred
     109 +if ($result.changed -and $update_schema) {
     110 + $ctor_args = [System.Collections.Generic.List`1[String]]@("LDAP://$($root_dse.dnsHostName)/RootDSE")
     111 + if ($null -ne $domain_username) {
     112 + $ctor_args.Add($domain_username) > $null
     113 + $ctor_args.Add($domain_password) > $null
     114 + }
     115 + $dse = New-Object -TypeName System.DirectoryServices.DirectoryEntry -ArgumentList $ctor_args.ToArray()
     116 + try {
     117 + $dse.Put("SchemaUpdateNow", 1)
     118 + if (-not $check_mode) {
     119 + $dse.SetInfo()
     120 + }
     121 + } finally {
     122 + $dse.Dispose()
     123 + }
     124 +}
     125 + 
     126 +Exit-Json -obj $result
     127 + 
     128 + 
  • ■ ■ ■ ■ ■ ■
    ansible/roles/laps/library/win_gpo.ps1
     1 +#!powershell
     2 + 
     3 +# Copyright: (c) 2018, Jordan Borean
     4 +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
     5 + 
     6 +#Requires -Module Ansible.ModuleUtils.Legacy
     7 + 
     8 +$ErrorActionPreference = "Stop"
     9 + 
     10 +$params = Parse-Args -arguments $args -supports_check_mode $true
     11 +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
     12 + 
     13 +$description = Get-AnsibleParam -obj $params -name "description" -type "str"
     14 +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
     15 +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "present"
     16 + 
     17 +$result = @{
     18 + changed = $false
     19 +}
     20 + 
     21 +$gpo = Get-GPO -Name $name -ErrorAction SilentlyContinue
     22 +if ($state -eq "absent" -and $null -ne $gpo) {
     23 + $result.id = $gpo.Id
     24 + $result.path = $gpo.Path
     25 + $gpo | Remove-GPO -WhatIf:$check_mode
     26 + $result.changed = $true
     27 +} elseif ($state -eq "present") {
     28 + if ($null -eq $gpo) {
     29 + $new_params = @{
     30 + Name = $name
     31 + WhatIf = $check_mode
     32 + }
     33 + if ($null -ne $description) {
     34 + $new_params.Comment = $description
     35 + }
     36 + $gpo = New-GPO @new_params
     37 + $result.changed = $true
     38 + }
     39 + 
     40 + # When creating a GPO in check mode these values won't be set
     41 + if ($null -ne $gpo) {
     42 + $result.id = $gpo.Id
     43 + $result.path = $gpo.Path
     44 + } else {
     45 + $result.id = [System.Guid]::Empty.ToString()
     46 + $result.path = "cn=$($result.id),cn=policies,cn=system,dc=check,dc=domain"
     47 + }
     48 +}
     49 + 
     50 +Exit-Json -obj $result
  • ■ ■ ■ ■ ■ ■
    ansible/roles/laps/library/win_gpo_link.ps1
     1 +#!powershell
     2 + 
     3 +# Copyright: (c) 2018, Jordan Borean
     4 +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
     5 + 
     6 +#Requires -Module Ansible.ModuleUtils.Legacy
     7 + 
     8 +$ErrorActionPreference = "Stop"
     9 + 
     10 +$params = Parse-Args -arguments $args -supports_check_mode $true
     11 +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
     12 + 
     13 +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
     14 +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -ValidateSet "absent", "present"
     15 +$enforced = Get-AnsibleParam -obj $params -name "enforced" -type "bool"
     16 +$enabled = Get-AnsibleParam -obj $params -name "enabled" -type "bool"
     17 +$target = Get-AnsibleParam -obj $params -name "target" -type "str"
     18 + 
     19 +if (-not $target) {
     20 + $target = (Get-ADRootDSE).defaultNamingContext
     21 +}
     22 + 
     23 +$result = @{
     24 + changed = $false
     25 +}
     26 + 
     27 +$link = (Get-GPInheritance -Target $target).GpoLinks | Where-Object { $_.DisplayName -eq $name }
     28 +if ($state -eq "present") {
     29 + if (-not $link) {
     30 + $link = New-GPLink -Name $name -Target $target -WhatIf:$check_mode
     31 + $result.changed = $true
     32 + }
     33 + 
     34 + if ($null -ne $enabled -and $link.Enabled -ne $enabled) {
     35 + $enabled_value = if ($enabled) { "Yes" } else { "No" }
     36 + $link = $link | Set-GPLink -LinkEnabled $enabled_value -WhatIf:$check_mode
     37 + $result.changed = $true
     38 + }
     39 + if ($null -ne $enforced -and $link.Enforced -ne $enforced) {
     40 + $enforced_value = if ($enforced) { "Yes" } else { "No" }
     41 + $link = $link | Set-GPLink -Enforced $enforced_value -WhatIf:$check_mode
     42 + $result.changed = $true
     43 + }
     44 +} else {
     45 + if ($link) {
     46 + $link | Remove-GPLink -WhatIf:$check_mode
     47 + $result.changed = $true
     48 + }
     49 +}
     50 + 
     51 +Exit-Json -obj $result
     52 + 
     53 + 
  • ■ ■ ■ ■ ■ ■
    ansible/roles/laps/library/win_gpo_reg.ps1
     1 +#!powershell
     2 + 
     3 +# Copyright: (c) 2018, Jordan Borean
     4 +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
     5 + 
     6 +#Requires -Module Ansible.ModuleUtils.Legacy
     7 + 
     8 +$ErrorActionPreference = "Stop"
     9 + 
     10 +$params = Parse-Args -arguments $args -supports_check_mode $true
     11 +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
     12 +$diff = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false
     13 + 
     14 +$gpo = Get-AnsibleParam -obj $params -name "gpo" -type "str" -failifempty $true
     15 +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
     16 +$path = Get-AnsibleParam -obj $params -name "path" -type "str" -failifempty $true
     17 +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "disabled", "present"
     18 +$type = Get-AnsibleParam -obj $params -name "type" -type "str" -default "string" -ValidateSet "string", "expandstring", "binary", "dword", "multistring", "qword"
     19 +$value = Get-AnsibleParam -obj $params -name "value"
     20 + 
     21 +$result = @{
     22 + changed = $false
     23 +}
     24 +if ($diff) {
     25 + $result.diff = @{
     26 + before = ""
     27 + after = ""
     28 + }
     29 +}
     30 + 
     31 +if ($state -in @("absent", "disabled") -and $null -ne $value) {
     32 + Fail-Json -obj $result -message "Cannot set a value with state=$state"
     33 +}
     34 + 
     35 +Function Convert-RegExportHexStringToByteArray {
     36 + <#
     37 + .SYNOPSIS
     38 + Simplified version of Convert-HexStringToByteArray from
     39 + https://cyber-defense.sans.org/blog/2010/02/11/powershell-byte-array-hex-convert
     40 + Expects a hex in the format you get when you run reg.exe export and
     41 + converts to a byte array so powershell can modify binary registry entries
     42 + 
     43 + .PARAMETER String
     44 + The string to convert in the format hex:be,ef,be,ef,be,ef,be,ef,be,ef
     45 + #>#
     46 + param(
     47 + [Parameter(Mandatory=$true)][String]$String
     48 + )
     49 + # Remove 'hex:' from the front of the string if present
     50 + $String = $String.ToLower() -replace '^hex\:',''
     51 + 
     52 + # Remove whitespace and any other non-hex crud.
     53 + $String = $String -replace '[^a-f0-9\\,x\-\:]',''
     54 + 
     55 + # Turn commas into colons
     56 + $String = $String -replace ',',':'
     57 + 
     58 + # Maybe there's nothing left over to convert...
     59 + if ($String.Length -eq 0) {
     60 + return ,@()
     61 + }
     62 + 
     63 + # Split string with or without colon delimiters.
     64 + if ($String.Length -eq 1) {
     65 + return ,@([System.Convert]::ToByte($String,16))
     66 + } elseif (($String.Length % 2 -eq 0) -and ($String.IndexOf(":") -eq -1)) {
     67 + return ,@($String -split '([a-f0-9]{2})' | ForEach-Object { if ($_) {[System.Convert]::ToByte($_,16)}})
     68 + } elseif ($String.IndexOf(":") -ne -1) {
     69 + return ,@($String -split ':+' | ForEach-Object {[System.Convert]::ToByte($_,16)})
     70 + } else {
     71 + return ,@()
     72 + }
     73 +}
     74 + 
     75 +Function Get-DiffValueString {
     76 + <#
     77 + .SYNOPSIS
     78 + Converts an input value to a string for use in a diff comparison.
     79 + 
     80 + .PARAMETER Type
     81 + The registry value type that is used to display the appropriate string
     82 + output.
     83 + 
     84 + .PARAMETER Value
     85 + The value to stringify.
     86 + #>
     87 + param(
     88 + [Parameter(Mandatory=$true)]
     89 + [ValidateSet("string", "expandstring", "binary", "dword", "multistring", "qword")]
     90 + [String]$Type,
     91 + [Parameter(Mandatory=$true)]$Value
     92 + )
     93 + if ($Type -eq "binary") {
     94 + $hex_values = [System.Collections.ArrayList]@()
     95 + foreach ($dec_value in $Value) {
     96 + $hex_values.Add("0x$("{0:x2}" -f $dec_value)") > $null
     97 + }
     98 + $diff_value = "[$($hex_values -join ", ")]"
     99 + } elseif ($Type -eq "dword") {
     100 + $diff_value = "0x$("{0:x8}" -f $Value)"
     101 + } elseif ($Type -eq "qword") {
     102 + $diff_value = "0x$("{0:x16}" -f $Value)"
     103 + } elseif ($Type -eq "multistring") {
     104 + $diff_value = "[$($Value -join ", ")]"
     105 + } else {
     106 + if ($Value.EndsWith([char]0x0000)) {
     107 + $Value = $Value.Substring(0, $Value.Length - 1)
     108 + }
     109 + 
     110 + $diff_value = $Value
     111 + }
     112 + 
     113 + return $diff_value
     114 +}
     115 + 
     116 +# Convert the input value to the type specified
     117 +if ($type -eq "binary") {
     118 + if ($null -eq $value) {
     119 + $value = ""
     120 + }
     121 + 
     122 + if ($value -is [String]) {
     123 + $value = [byte[]](Convert-RegExportHexStringToByteArray -String $value)
     124 + } elseif ($value -is [Int]) {
     125 + if ($value -gt 255) {
     126 + Fail-Json -obj $result -message "Cannot convert binary value '$value' to byte array, please specify this value as a yaml byte array or comma separated hex value string"
     127 + }
     128 + $value = [byte[]]@([byte]$value)
     129 + } elseif ($value -is [Array]) {
     130 + $value = [byte[]]$value
     131 + }
     132 +} elseif ($type -in @("dword", "qword")) {
     133 + # dword's and dword's don't allow null values, set to 0
     134 + if ($null -eq $value) {
     135 + $value = 0
     136 + }
     137 + 
     138 + if ($value -is [String]) {
     139 + # If the value is a string we need to convert it to an unsigned int64
     140 + # it needs to be unsigned as Ansible passes in an unsigned value while
     141 + # powershell uses a signed value type. The value will then be converted
     142 + # below
     143 + $value = [UInt64]$value
     144 + }
     145 + 
     146 + if ($type -eq "dword") {
     147 + if ($value -gt [UInt32]::MaxValue) {
     148 + Fail-Json -obj $result -message "value cannot be larger than 0xffffffff when type is dword"
     149 + } elseif ($value -gt [Int32]::MaxValue) {
     150 + # When dealing with larger int32 (> 2147483647 or 0x7FFFFFFF) PowerShell
     151 + # automatically converts it to a signed int64. We need to convert this to
     152 + # signed int32 by parsing the hex string value.
     153 + $value = "0x$("{0:x}" -f $value)"
     154 + }
     155 + $value = [Int32]$value
     156 + } else {
     157 + if ($value -gt [UInt64]::MaxValue) {
     158 + Fail-Json -obj $result -message "value cannot be larger than 0xffffffffffffffff when type is qword"
     159 + } elseif ($value -gt [Int64]::MaxValue) {
     160 + $value = "0x$("{0:x}" -f $value)"
     161 + }
     162 + $value = [Int64]$value
     163 + }
     164 +} elseif ($type -in @("string", "expandstring")) {
     165 + # A null string or expandstring must be empty quotes
     166 + if ($null -eq $value) {
     167 + $value = ""
     168 + }
     169 +} elseif ($type -eq "multistring") {
     170 + # Convert the value for a multistring to a String[] array
     171 + if ($null -eq $value) {
     172 + $value = [String[]]@()
     173 + } elseif ($value -isnot [Array]) {
     174 + $new_value = New-Object -TypeName String[] -ArgumentList 1
     175 + $new_value[0] = $value.ToString([CultureInfo]::InvariantCulture)
     176 + $value = $new_value
     177 + } else {
     178 + $new_value = New-Object -TypeName String[] -ArgumentList $value.Count
     179 + foreach ($entry in $value) {
     180 + $new_value[$value.IndexOf($entry)] = $entry.ToString([CultureInfo]::InvariantCulture)
     181 + }
     182 + $value = $new_value
     183 + }
     184 +}
     185 + 
     186 +$existing_value = Get-GPRegistryValue -Name $gpo -Key $path -ValueName $name -ErrorAction SilentlyContinue
     187 +if ($null -ne $existing_value -and $diff) {
     188 + $result.diff.before = @{
     189 + disabled = ($existing_value.PolicyState -eq "Delete")
     190 + gpo = $gpo
     191 + name = $existing_value.ValueName
     192 + path = $existing_value.KeyPath
     193 + type = $existing_value.Type.ToString()
     194 + value = (Get-DiffValueString -Type $existing_value.Type.ToString() -Value $existing_value.Value)
     195 + }
     196 +}
     197 + 
     198 +if ($state -eq "absent") {
     199 + if ($null -ne $existing_value) {
     200 + Remove-GPRegistryValue -Name $gpo -Key $path -ValueName $name -WhatIf:$check_mode > $null
     201 + $result.changed = $true
     202 + }
     203 +} else {
     204 + $common_params = @{
     205 + Name = $gpo
     206 + Key = $path
     207 + ValueName = $name
     208 + WhatIf = $check_mode
     209 + }
     210 + 
     211 + if ($null -eq $existing_value) {
     212 + if ($state -eq "disabled") {
     213 + $common_params.Disable = $true
     214 + } else {
     215 + $common_params.Value = $value
     216 + $common_params.Type = $type
     217 + }
     218 + 
     219 + Set-GPRegistryValue @common_params > $null
     220 + $result.changed = $true
     221 + } elseif ($state -eq "disabled") {
     222 + if ($existing_value.PolicyState -ne "Delete") {
     223 + Set-GPRegistryValue -Disable @common_params > $null
     224 + $result.changed = $true
     225 + }
     226 + } elseif ($existing_value.PolicyState -eq "Delete") {
     227 + # If the previous state was disabled then we need to remove that value
     228 + # before we set the new one as it will cause double ups on the key
     229 + Remove-GPRegistryValue @common_params > $null
     230 + Set-GPRegistryValue -Value $value -Type $type @common_params > $null
     231 + $result.changed = $true
     232 + } else {
     233 + $before_type = $existing_value.Type.ToString()
     234 + $before_value = Get-DiffValueString -Type $before_type -Value $existing_value.Value
     235 + $new_value = Get-DiffValueString -Type $type -Value $value
     236 + 
     237 + if (($before_value -ne $new_value) -or ($before_type -ne $type)) {
     238 + Set-GPRegistryValue -Value $value -Type $type @common_params > $null
     239 + $result.changed = $true
     240 + }
     241 + }
     242 + 
     243 + if ($diff) {
     244 + if ($check_mode) {
     245 + # in check mode we won't have access to the new values so just used the inputs
     246 + $result.diff.after = @{
     247 + disabled = ($state -eq "disabled")
     248 + gpo = $gpo
     249 + name = $name
     250 + path = $path
     251 + type = $type
     252 + value = (Get-DiffValueString -Type $type -Value $value)
     253 + }
     254 + } else {
     255 + $new_value = Get-GPRegistryValue -Name $gpo -Key $path -ValueName $name
     256 + $result.diff.after = @{
     257 + disabled = ($new_value.PolicyState -eq "Delete")
     258 + gpo = $gpo
     259 + name = $new_value.ValueName
     260 + path = $new_value.KeyPath
     261 + type = $new_value.Type.ToString()
     262 + value = (Get-DiffValueString -Type $new_value.Type.ToString() -Value $new_value.Value)
     263 + }
     264 + }
     265 + }
     266 +}
     267 + 
     268 +Exit-Json -obj $result
     269 + 
     270 + 
  • ■ ■ ■ ■ ■ ■
    ansible/roles/laps/tasks/main.yml
     1 +---
     2 +- name: Move SRV02 to Workstations OU
     3 + win_shell: |
     4 + try {
     5 + Get-ADOrganizationalUnit -Identity "OU=Workstations,DC=north,DC=sevenkingdoms,DC=local" > $null
     6 + Move-ADObject -Identity "CN=CASTELBLACK,CN=Computers,DC=north,DC=sevenkingdoms,DC=local" -TargetPath "OU=Workstations,DC=north,DC=sevenkingdoms,DC=local"
     7 + $true
     8 + } catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
     9 + $false
     10 + }
     11 + when: move_computer == 'dc02'
     12 + 
     13 +- name: Move SRV03 to Workstations OU
     14 + win_shell: |
     15 + try {
     16 + Get-ADOrganizationalUnit -Identity "OU=Workstations,DC=essos,DC=local" > $null
     17 + Move-ADObject -Identity "CN=BRAAVOS,CN=Computers,DC=essos,DC=local" -TargetPath "OU=Workstations,DC=essos,DC=local"
     18 + $true
     19 + } catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
     20 + $false
     21 + }
     22 + when: move_computer == 'dc03'
     23 + 
     24 +- name: Install LAPS Package on Servers
     25 + ansible.windows.win_package:
     26 + arguments: "ADDLOCAL=Management.ADMX"
     27 + path: https://download.microsoft.com/download/C/7/A/C7AAD914-A8A6-4904-88A1-29E657445D03/LAPS.x64.msi
     28 + state: present
     29 + creates_path: "%ProgramFiles%\\LAPS"
     30 + register: pri_laps_install
     31 + until: pri_laps_install is success
     32 + retries: 3 # Try 3 times just in case it failed to download the URL
     33 + delay: 1
     34 + when: prep_servers | bool
     35 + 
     36 +- name: Reboot After Installing LAPS on Servers
     37 + ansible.windows.win_reboot:
     38 + when: prep_servers | bool and pri_laps_install.reboot_required
     39 + 
     40 +- name: Configure Password Properties
     41 + win_ad_object:
     42 + name: ms-Mcs-AdmPwd
     43 + attributes:
     44 + adminDescription: LAPS Password Attribute
     45 + lDAPDisplayName: ms-Mcs-AdmPwd
     46 + adminDisplayName: ms-Mcs-AdmPwd
     47 + attributeId: 1.2.840.113556.1.8000.2554.50051.45980.28112.18903.35903.6685103.1224907.2.1
     48 + attributeSyntax: '2.5.5.5' # String(IAS)
     49 + omSyntax: 19 # String(Printable)
     50 + isSingleValued: True
     51 + systemOnly: False
     52 + isMemberOfPartialAttributeSet: False
     53 + searchFlags: 904 # RO,NV,CF,PR - http://www.frickelsoft.net/blog/?p=151
     54 + showInAdvancedViewOnly: False
     55 + context: schema
     56 + type: attribute
     57 + update_schema: True
     58 + # privileges required to update the schema attributes
     59 + become: yes
     60 + become_method: runas
     61 + become_user: SYSTEM
     62 + when: prep_servers | bool
     63 + 
     64 +- name: Configure Password Expiry Time
     65 + win_ad_object:
     66 + name: ms-Mcs-AdmPwdExpirationTime
     67 + attributes:
     68 + adminDescription: LAPS Password Expiration Time Attribute
     69 + lDAPDisplayName: ms-Mcs-AdmPwdExpirationTime
     70 + adminDisplayName: ms-Mcs-AdmPwdExpirationTime
     71 + attributeId: 1.2.840.113556.1.8000.2554.50051.45980.28112.18903.35903.6685103.1224907.2.2
     72 + attributeSyntax: '2.5.5.16' # LargeInteger
     73 + omSyntax: 65 # LargeInteger
     74 + isSingleValued: True
     75 + systemOnly: False
     76 + isMemberOfPartialAttributeSet: False
     77 + searchFlags: 0
     78 + showInAdvancedViewOnly: False
     79 + context: schema
     80 + type: attribute
     81 + update_schema: True
     82 + become: yes
     83 + become_method: runas
     84 + become_user: SYSTEM
     85 + when: prep_servers | bool
     86 + 
     87 +- name: Add LAPS attributes to the Computer Attribute
     88 + win_ad_object:
     89 + name: Computer
     90 + may_contain:
     91 + - ms-Mcs-AdmPwd
     92 + - ms-Mcs-AdmPwdExpirationTime
     93 + context: schema
     94 + update_schema: True
     95 + become: yes
     96 + become_method: runas
     97 + become_user: SYSTEM
     98 + when: prep_servers | bool
     99 + 
     100 +- name: Apply DACL to OU Containers (north.sevenkingdoms.local)
     101 + win_ad_dacl:
     102 + path: 'OU=Workstations,DC=north,DC=sevenkingdoms,DC=local'
     103 + state: present
     104 + aces:
     105 + - rights:
     106 + - ReadProperty
     107 + - WriteProperty
     108 + inheritance_type: Descendents
     109 + inherited_object_type: Computer
     110 + object_type: ms-Mcs-AdmPwdExpirationTime
     111 + access: allow
     112 + account: S-1-5-10 # NT AUTHORITY\SELF
     113 + - rights: WriteProperty
     114 + inheritance_type: Descendents
     115 + inherited_object_type: Computer
     116 + object_type: ms-Mcs-AdmPwd
     117 + access: allow
     118 + account: S-1-5-10
     119 + when: apply_dacl == 'dc02'
     120 + 
     121 +- name: Apply DACL to OU Containers (essos.local)
     122 + win_ad_dacl:
     123 + path: 'OU=Workstations,DC=essos,DC=local'
     124 + state: present
     125 + aces:
     126 + - rights:
     127 + - ReadProperty
     128 + - WriteProperty
     129 + inheritance_type: Descendents
     130 + inherited_object_type: Computer
     131 + object_type: ms-Mcs-AdmPwdExpirationTime
     132 + access: allow
     133 + account: S-1-5-10 # NT AUTHORITY\SELF
     134 + - rights: WriteProperty
     135 + inheritance_type: Descendents
     136 + inherited_object_type: Computer
     137 + object_type: ms-Mcs-AdmPwd
     138 + access: allow
     139 + account: S-1-5-10
     140 + when: apply_dacl == 'dc03'
     141 + 
     142 +- name: Create LAPS GPO
     143 + win_gpo:
     144 + name: '{{ opt_laps_gpo_name }}'
     145 + description: Setup by Ansible for LAPS
     146 + state: present
     147 + register: pri_laps_gpo
     148 + when: create_gpo | bool
     149 + 
     150 +- name: Add LAPS extension to GPO
     151 + win_ad_object:
     152 + name: '{{ pri_laps_gpo.path }}'
     153 + attributes:
     154 + # [Registry:Admin Tool][AdmPwd:Admin Tool]
     155 + gPCMachineExtensionNames: "[{35378EAC-683F-11D2-A89A-00C04FBBCFA2}{D02B1F72-3407-48AE-BA88-E8213C6761F1}]\
     156 + [{D76B9641-3288-4F75-942D-087DE603E3EA}{D02B1F72-3407-48AE-BA88-E8213C6761F1}]"
     157 + when: create_gpo | bool
     158 + 
     159 +- name: Configure Password Policy Settings on GPO
     160 + win_gpo_reg:
     161 + gpo: '{{ opt_laps_gpo_name }}'
     162 + name: '{{ item.name }}'
     163 + path: 'HKLM\Software\Policies\Microsoft Services\AdmPwd'
     164 + state: present
     165 + type: dword
     166 + value: '{{ item.value }}'
     167 + with_items:
     168 + - name: PasswordComplexity
     169 + value: 4
     170 + - name: PasswordLength
     171 + value: 14
     172 + - name: PasswordAgeDays
     173 + value: 30
     174 + when: create_gpo | bool
     175 + 
     176 +- name: Configure Expiration Protection on GPO
     177 + win_gpo_reg:
     178 + gpo: '{{ opt_laps_gpo_name }}'
     179 + name: PwdExpirationProtectionEnabled
     180 + path: 'HKLM\Software\Policies\Microsoft Services\AdmPwd'
     181 + state: present
     182 + type: dword
     183 + value: 1
     184 + when: create_gpo | bool
     185 + 
     186 +- name: Remove Configuration for Expiration Protection on GPO
     187 + win_gpo_reg:
     188 + gpo: '{{ opt_laps_gpo_name }}'
     189 + name: PwdExpirationProtectionEnabled
     190 + path: 'HKLM\Software\Policies\Microsoft Services\AdmPwd'
     191 + state: absent
     192 + when: create_gpo | bool
     193 + 
     194 +- name: Configure Custom Admin Username Policy on GPO
     195 + win_gpo_reg:
     196 + gpo: '{{ opt_laps_gpo_name }}'
     197 + name: AdminAccountName
     198 + path: 'HKLM\Software\Policies\Microsoft Services\AdmPwd'
     199 + state: present
     200 + type: string
     201 + when: create_gpo | bool
     202 + 
     203 +- name: Enable the GPO
     204 + win_gpo_reg:
     205 + gpo: '{{ opt_laps_gpo_name }}'
     206 + name: AdmPwdEnabled
     207 + path: 'HKLM\Software\Policies\Microsoft Services\AdmPwd'
     208 + state: present
     209 + type: dword
     210 + value: 1
     211 + when: create_gpo | bool
     212 + 
     213 +- name: Create Comment File for GPO
     214 + ansible.windows.win_copy:
     215 + src: ../files/comment.cmtx
     216 + dest: C:\Windows\SYSVOL\domain\Policies\{{ '{' }}{{ pri_laps_gpo.id }}{{ '}' }}\Machine\comment.cmtx
     217 + when: create_gpo | bool
     218 + 
     219 +- name: Ensure GPO is Linked
     220 + win_gpo_link:
     221 + name: '{{ opt_laps_gpo_name }}'
     222 + target: 'OU=Workstations,DC=north,DC=sevenkingdoms,DC=local'
     223 + state: present
     224 + enforced: True
     225 + enabled: True
     226 + when: gpo_linked == 'dc02'
     227 + 
     228 +- name: Ensure GPO is Linked
     229 + win_gpo_link:
     230 + name: '{{ opt_laps_gpo_name }}'
     231 + target: 'OU=Workstations,DC=essos,DC=local'
     232 + state: present
     233 + enforced: True
     234 + enabled: True
     235 + when: gpo_linked == 'dc03'
     236 + 
     237 +- name: Install to Servers
     238 + ansible.windows.win_package:
     239 + arguments: "ADDLOCAL=CSE"
     240 + path: https://download.microsoft.com/download/C/7/A/C7AAD914-A8A6-4904-88A1-29E657445D03/LAPS.x64.msi
     241 + state: present
     242 + creates_path: "%ProgramFiles%\\LAPS"
     243 + register: pri_laps_install
     244 + until: pri_laps_install is success
     245 + retries: 3 # Try 3 times just in case it failed to download the URL
     246 + delay: 1
     247 + when: install_servers | bool
     248 + 
     249 +- name: reboot after installing LAPS if required
     250 + ansible.windows.win_reboot:
     251 + when: install_servers | bool and pri_laps_install.reboot_required
     252 + 
     253 +- name: Refresh GPO on the Clients
     254 + ansible.windows.win_command: gpupdate /force
     255 + when: install_servers | bool
     256 + 
     257 +- name: Retrieve LAPS Password on DC02
     258 + win_shell: |
     259 + $obj = Get-ADObject -Identity "CN=CASTELBLACK,OU=Workstations,DC=north,DC=sevenkingdoms,DC=local" -Properties ms-Mcs-AdmPwd
     260 + Write-Output -InputObject $obj."ms-Mcs-AdmPwd"
     261 + register: powershell_password
     262 + changed_when: False
     263 + when: test_deployment == 'dc02'
     264 + 
     265 +- name: Retrieve LAPS Password on DC03
     266 + win_shell: |
     267 + $obj = Get-ADObject -Identity "CN=BRAAVOS,OU=Workstations,DC=essos,DC=local" -Properties ms-Mcs-AdmPwd
     268 + Write-Output -InputObject $obj."ms-Mcs-AdmPwd"
     269 + register: powershell_password
     270 + changed_when: False
     271 + when: test_deployment == 'dc03'
  • ■ ■ ■ ■ ■ ■
    ansible/roles/laps/vars/main.yml
     1 +---
     2 +# converts opt_laps_password_policy_complexity to the value expected by GPO
     3 +pri_laps_password_policy_complexity:
     4 + uppercase: 1
     5 + uppercase,lowercase: 2
     6 + uppercase,lowercase,digits: 3
     7 + uppercase,lowercase,digits,symbols: 4
     8 + 
     9 +# GPO variables
     10 +opt_laps_gpo_name: ansible-laps
     11 +opt_laps_password_policy_complexity: uppercase,lowercase,digits,symbols
     12 +opt_laps_password_policy_length: 14
     13 +opt_laps_password_policy_age: 30
Please wait...
Page is in error, reload to recover