Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
eea3e30
feat: add provider abstraction for ludus and proxmox with dynamic CLI…
l50 Apr 25, 2026
13135c1
feat: improve inventory sync to handle hostnames and IPs for multiple…
l50 Apr 25, 2026
c5b10a2
test: add unit tests for inventory update logic and host role extraction
l50 Apr 25, 2026
659c21d
fix: separate stdout and stderr capture to avoid log pollution in lud…
l50 Apr 25, 2026
f44c250
feat: add ADCS ESC6, ESC7, ESC10, ESC11, ESC13, and ESC15 checks and …
l50 Apr 25, 2026
770b0a2
fix: correct CA manager ACL check to use proper identity and rights p…
l50 Apr 25, 2026
4965cfa
feat: add health check retry logic and ludus client regression tests
l50 Apr 25, 2026
612ef25
feat: add retry logic for provider command execution with tests
l50 Apr 25, 2026
ff7c54e
fix: improve scheduled task validation for missing and unreadable tasks
l50 Apr 26, 2026
41ec3be
fix: correct switch statement to properly match scheduled task states
l50 Apr 27, 2026
d5f418c
Merge branch 'main' into feat/provider-agnostic-provisioning
l50 Apr 27, 2026
3078a17
fix: improve ps command reliability and add terragrunt_fmt to pre-commit
l50 Apr 27, 2026
b4478c0
feat: add support for remote Ludus execution via SSH with inline ansi…
l50 Apr 27, 2026
ca426b7
refactor: simplify switch statements in checkAnonymousSMB and checkLLMNR
l50 Apr 27, 2026
8a3644f
ci: add terragrunt installation step to pre-commit workflow
l50 Apr 27, 2026
dd054a3
feat: add AD object allowlist and unmanaged-object purge to lab reset
l50 Apr 27, 2026
4fc465c
chore: update example SSM bucket names in inventory files
l50 Apr 27, 2026
17c5057
feat: add SSH-mode support for ludus provider and update doctor checks
l50 Apr 27, 2026
0a14a40
feat: support ssh_config Host aliases for Ludus SSH connections
l50 Apr 27, 2026
3ac3c17
feat: add interactive init wizard for configuration and update ludus …
l50 Apr 27, 2026
1ac9578
feat: add up orchestrator command and document end-to-end lab workflow
l50 Apr 27, 2026
826a9ca
refactor: unset ssh_user and ssh_port defaults to improve alias detec…
l50 Apr 27, 2026
3591dbd
refactor: switch trust creation to netdom, simplify logic and update …
l50 Apr 27, 2026
4d42fca
feat: add Ludus SSH provisioning with SOCKS5 tunnel and CLI auto-install
l50 Apr 27, 2026
153bf5d
fix: handle EPIPE error when writing to stdout in validator
l50 Apr 27, 2026
e76e7bc
feat: add credential, group, and local admin validation checks to val…
l50 Apr 27, 2026
728655a
feat: replace OpenSSH subprocess with pure-Go SSH client and SOCKS5 p…
l50 Apr 27, 2026
ebec0a8
refactor: switch group creation tasks to microsoft.ad.group module
l50 Apr 28, 2026
7468e96
refactor: unify ssh client abstraction and simplify SOCKS5 tunnel logic
l50 Apr 28, 2026
8d9e22c
```
l50 Apr 28, 2026
27907f4
perf: optimize runPS by removing retries and caching failed hosts
l50 Apr 28, 2026
dbf05c5
fix: improve error reporting and concurrency safety in command execution
l50 Apr 28, 2026
45685af
feat: add per-host retry and failure threshold to runPS for resilient…
l50 Apr 28, 2026
5b8debf
feat: add direct WinRM client for PowerShell over SSH tunnel in ludus…
l50 Apr 28, 2026
f239575
build: update dependencies and promote winrm to direct dependency
l50 Apr 28, 2026
716d442
feat: introduce robust PowerShell script runner and JSON-based host v…
l50 Apr 28, 2026
22ba3b8
feat: improve labmap and validator for ADCS ESC7 and LDAP registry va…
l50 Apr 28, 2026
c51841b
refactor: improve DNS forwarder check diagnostics and error handling
l50 Apr 28, 2026
9ea0e08
refactor: improve error handling, user prompts, and file operations i…
l50 Apr 29, 2026
101e15c
Merge remote-tracking branch 'origin/main' into feat/provider-agnosti…
l50 Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/pre-commit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ jobs:
with:
terraform_version: "1.9.7"

- name: Set up Terragrunt
run: |
TG_VERSION="v0.69.1"
curl -fsSL -o /tmp/terragrunt \
"https://github.com/gruntwork-io/terragrunt/releases/download/${TG_VERSION}/terragrunt_linux_amd64"
sudo install -m 0755 /tmp/terragrunt /usr/local/bin/terragrunt
terragrunt --version

- name: Set up TFLint
uses: terraform-linters/setup-tflint@b480b8fcdaa6f2c577f8e4fa799e89e756bb7c93 # v6
with:
Expand Down
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ repos:
hooks:
- id: terraform_fmt
files: ^(modules|infra)/
- id: terragrunt_fmt
files: ^infra/
- id: terraform_validate
files: ^modules/
args:
Expand Down
16 changes: 11 additions & 5 deletions ad/GOAD/data/dev-overlay.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"hosts": {
"dc01": {
"vulns": [
"disable_firewall"
"disable_firewall",
"adcs_esc10_case1",
"adcs_esc10_case2"
]
},
"dc02": {
Expand Down Expand Up @@ -60,9 +62,11 @@
},
"vulns": [
"ntlmdowngrade",
"disable_firewall"
],
"vulns_vars": null
"disable_firewall",
"adcs_esc7",
"adcs_esc13",
"adcs_esc15"
]
},
"srv02": {
"vulns": [
Expand All @@ -79,7 +83,9 @@
"srv03": {
"vulns": [
"openshares",
"disable_firewall"
"disable_firewall",
"adcs_esc6",
"adcs_esc11"
]
}
}
Expand Down
16 changes: 11 additions & 5 deletions ad/GOAD/data/staging-overlay.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
"dc01": {
"local_admin_password": "ykRXQ@rWNV4znesz-h!c",
"vulns": [
"disable_firewall"
"disable_firewall",
"adcs_esc10_case1",
"adcs_esc10_case2"
]
},
"dc02": {
Expand Down Expand Up @@ -55,9 +57,11 @@
},
"vulns": [
"ntlmdowngrade",
"disable_firewall"
],
"vulns_vars": null
"disable_firewall",
"adcs_esc7",
"adcs_esc13",
"adcs_esc15"
]
},
"srv02": {
"local_admin_password": "moydNed_wEKuP8KN6rUx"
Expand All @@ -66,7 +70,9 @@
"local_admin_password": "-cwuyGW494yZnC_M8wLN",
"vulns": [
"openshares",
"disable_firewall"
"disable_firewall",
"adcs_esc6",
"adcs_esc11"
]
}
}
Expand Down
16 changes: 11 additions & 5 deletions ad/GOAD/data/test-overlay.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"hosts": {
"dc01": {
"vulns": [
"disable_firewall"
"disable_firewall",
"adcs_esc10_case1",
"adcs_esc10_case2"
]
},
"dc02": {
Expand Down Expand Up @@ -45,14 +47,18 @@
},
"vulns": [
"ntlmdowngrade",
"disable_firewall"
],
"vulns_vars": null
"disable_firewall",
"adcs_esc7",
"adcs_esc13",
"adcs_esc15"
]
},
"srv03": {
"vulns": [
"openshares",
"disable_firewall"
"disable_firewall",
"adcs_esc6",
"adcs_esc11"
]
}
}
Expand Down
140 changes: 140 additions & 0 deletions ad/GOAD/providers/ludus/inventory
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,150 @@ dc03 ansible_host={{ip_range}}.12 dns_domain=dc03 dict_key=dc03
srv03 ansible_host={{ip_range}}.23 dns_domain=dc03 dict_key=srv03

[all:vars]
; domain_name : folder inside ad/
domain_name=GOAD

; administrator user
admin_user=administrator

; global settings inventory default value
keyboard_layouts=["en-US", "da-DK", "fr-FR"]

; modify this to add a default route
add_route=no
route_gateway=192.168.56.1
route_network=10.0.0.0/8

; modify this to enable http proxy
enable_http_proxy=no
ad_http_proxy=http://x.x.x.x:xxxx
ad_https_proxy=http://x.x.x.x:xxxx

;force_dns_server
force_dns_server=no
dns_server={{ip_range}}.254

;dns server forwarder
dns_server_forwarder={{ip_range}}.254

; winrm connection (windows)
ansible_user=localuser
ansible_password=password
ansible_connection=winrm
ansible_winrm_server_cert_validation=ignore
ansible_winrm_operation_timeout_sec=400
ansible_winrm_read_timeout_sec=500

; LAB SCENARIO CONFIGURATION -----------------------------

; computers inside domain (mandatory)
; usage : build.yml, ad-relations.yml, ad-servers.yml, vulnerabilities.yml
[domain]
dc01
dc02
dc03
srv02
srv03

[linux_domain]

; domain controller (mandatory)
; usage : ad-acl.yml, ad-data.yml, ad-relations.yml, laps.yml
[dc]
dc01
dc02
dc03

; domain server to enroll (mandatory if you want servers)
; usage : ad-data.yml, ad-servers.yml, laps.yml
[server]
srv02
srv03

; workstation to enroll (mandatory if you want workstation)
; usage : ad-servers.yml, laps.yml
[workstation]

; parent domain controller (mandatory)
; usage : ad-servers.yml
[parent_dc]
dc01
dc03

; child domain controller (need a fqdn child_name.parent_name)
; usage : ad-servers.yml
[child_dc]
dc02

; external trust, need domain trust entry in config (bidirectionnal)
; usage : ad-trusts.yml
[trust]
dc01
dc03

; install adcs
; usage : adcs.yml
[adcs]
dc01
srv03

; install custom template (dc)
; usage : adcs.yml
[adcs_customtemplates]
dc03

; install iis with default website asp upload on 80
; usage : servers.yml
[iis]
srv02

; install mssql
; usage : servers.yml
[mssql]
srv02
srv03

; install mssql gui
; usage : servers.yml
[mssql_ssms]
srv02

; install webdav
[webdav]
srv02
srv03

[laps_dc]
dc03
[laps_server]
srv03
[laps_workstation]

; allow computer update
; usage : update.yml
[update]
srv02

; disable update
; usage : update.yml
[no_update]
dc01
dc02
dc03
srv03

; allow defender
; usage : security.yml
[defender_on]
dc01
dc02
dc03
srv03

; disable defender
; usage : security.yml
[defender_off]
srv02

;stay empty until override
[extensions]
1 change: 0 additions & 1 deletion ansible/playbooks/ad-trusts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
- { role: 'dreadnode.goad.trusts', tags: 'trust' }
vars:
domain: "{{ lab.hosts[dict_key].domain }}"
domain_username: "{{ domain }}\\{{ admin_user }}"
domain_password: "{{ lab.domains[domain].domain_password }}"
remote_forest: "{{ lab.domains[domain].trust }}"
remote_admin: "{{ admin_user }}@{{ remote_forest }}"
Expand Down
28 changes: 28 additions & 0 deletions ansible/playbooks/load_network_mappings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
# Pre-flight for `lab reset` (and any AD-state-only run): populates the
# host_ipv4 / dc_ipv4 / instance_to_ip / dc_hostname_to_ip facts that
# downstream plays read via hostvars.
#
# Full provision runs network_setup.yml which gathers facts from each
# Windows host. lab reset skips that for speed and instead reuses the
# AWS instance mapping JSON the CLI writes to /tmp before the run, via
# the same network_discovery role task files network_setup uses.

- name: Load AWS mappings and DC facts for AD-state plays
hosts: all
gather_facts: false
tasks:
- name: Apply AWS instance mapping
ansible.builtin.include_role:
name: dreadnode.goad.network_discovery
tasks_from: aws_mapping.yml

- name: Store DC IPs as facts
ansible.builtin.include_role:
name: dreadnode.goad.network_discovery
tasks_from: dc_facts.yml

- name: Build and distribute dc_hostname_to_ip mapping
ansible.builtin.include_role:
name: dreadnode.goad.network_discovery
tasks_from: dc_hostname_mapping.yml
4 changes: 4 additions & 0 deletions ansible/playbooks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@
# specifics security linked to the scenario are here
- name: Import Security
ansible.builtin.import_playbook: security.yml
# enables LDAP diagnostic logging on DCs and SQL audit on MSSQL hosts so
# attack reconnaissance produces detectable telemetry
- name: Import Security Logging
ansible.builtin.import_playbook: security_logging.yml
# --------------------------------------------------------------------
# specifics vulns linked to the scenario are here
- name: Import Vulnerabilities
Expand Down
19 changes: 10 additions & 9 deletions ansible/playbooks/network_setup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,11 @@
gather_facts: false
serial: 10
tasks:
- name: Build and distribute DC and instance IP mappings
# instance_to_ip is set by network_discovery's aws_mapping.yml on SSM hosts.
# For non-SSM connections (where ansible_host is already an IP/hostname),
# rebuild it from per-host host_ipv4 facts.
- name: Build instance_to_ip fallback for non-SSM connections
ansible.builtin.set_fact:
dc_hostname_to_ip: >-
{%- set mapping = {} -%}
{%- for host in groups['dc'] -%}
{%- if hostvars[host]['dc_ipv4'] is defined and hostvars[host]['dc_ipv4'] != '' -%}
{%- set _ = mapping.update({host: hostvars[host]['dc_ipv4']}) -%}
{%- endif -%}
{%- endfor -%}
{{ mapping }}
instance_to_ip: >-
{%- set mapping = {} -%}
{%- for host in groups['all'] -%}
Expand All @@ -40,6 +35,12 @@
delegate_to: "{{ item }}"
loop: "{{ groups['all'] }}"
throttle: 5
when: ansible_connection is not search('aws_ssm')

- name: Build and distribute dc_hostname_to_ip mapping
ansible.builtin.include_role:
name: dreadnode.goad.network_discovery
tasks_from: dc_hostname_mapping.yml

- name: Debug the mappings
ansible.builtin.debug:
Expand Down
9 changes: 3 additions & 6 deletions ansible/roles/ad/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,9 @@ Configure Active Directory domain administrator membership and settings

### groups.yml

- **Create Universal Groups** (ansible.windows.win_powershell) - Conditional
- **Wait for Universal group creation to complete** (ansible.builtin.async_status) - Conditional
- **Create Global Groups** (ansible.windows.win_powershell) - Conditional
- **Wait for Global group creation to complete** (ansible.builtin.async_status) - Conditional
- **Create DomainLocal Groups** (ansible.windows.win_powershell) - Conditional
- **Wait for DomainLocal group creation to complete** (ansible.builtin.async_status) - Conditional
- **Create Universal Groups** (microsoft.ad.group) - Conditional
- **Create Global Groups** (microsoft.ad.group) - Conditional
- **Create DomainLocal Groups** (microsoft.ad.group) - Conditional

### main.yml

Expand Down
Loading
Loading