Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 6 additions & 1 deletion .github/workflows/pre-commit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,14 @@ jobs:

- name: Install Ansible collections
run: |
ansible-galaxy collection install -r requirements.yml --force
ansible-galaxy collection install -r ansible/requirements.yml --force

- name: Build and install collection locally
working-directory: ansible
run: |
ansible-galaxy collection build --force
ansible-galaxy collection install dreadnode-goad-*.tar.gz -p ~/.ansible/collections --force --pre

- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
Expand Down
222 changes: 222 additions & 0 deletions .github/workflows/syntax-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
---
name: Ansible Syntax Check
on:
merge_group:
pull_request:
branches:
- main
types:
- opened
- synchronize
- reopened
push:
branches:
- main
schedule:
# Runs every Sunday at 4 AM (see https://crontab.guru)
- cron: "0 4 * * 0"
workflow_dispatch:
inputs:
ROLE:
description: 'Role to test (e.g. "elk", "ad", "vulns_acls")'
required: false
default: ''
type: string

concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}

env:
ANSIBLE_FORCE_COLOR: "1"
COLLECTION_NAMESPACE: dreadnode
COLLECTION_NAME: goad
COLLECTION_PATH: ansible_collections/dreadnode/goad
REQUIREMENTS_FILE: .hooks/requirements.txt
PY_COLORS: "1"
PYTHON_VERSION: "3.14.3"
ROLE: ${{ github.event.inputs.ROLE }}
ANSIBLE_COLLECTIONS_PATH: ~/.ansible/collections

jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
roles: ${{ steps.detect.outputs.roles }}
test_all: ${{ steps.check-event.outputs.test_all }}
steps:
- name: Checkout git repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: ${{ env.COLLECTION_PATH }}
fetch-depth: 0

- name: Check event type
id: check-event
run: |
if [[ "${{ github.event_name }}" == "push" ]] || \
[[ "${{ github.event_name }}" == "schedule" ]] || \
[[ "${{ github.event_name }}" == "merge_group" ]] || \
[[ "${{ github.event_name }}" == "workflow_dispatch" && -z "${{ env.ROLE }}" ]]; then
echo "test_all=true" >> "$GITHUB_OUTPUT"
else
echo "test_all=false" >> "$GITHUB_OUTPUT"
fi

- name: Detect changed roles
id: detect
if: steps.check-event.outputs.test_all == 'false'
working-directory: ${{ env.COLLECTION_PATH }}
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
else
BASE="origin/main"
HEAD="HEAD"
fi

CHANGED_FILES=$(git diff --name-only "$BASE"..."$HEAD")
echo "Changed files:"
echo "$CHANGED_FILES"

ROLES=$(echo "$CHANGED_FILES" | grep '^ansible/roles/' | cut -d'/' -f3 | sort -u | tr '\n' ' ')
echo "roles=$ROLES" >> "$GITHUB_OUTPUT"
echo "Changed roles: $ROLES"

validate-inputs:
runs-on: ubuntu-latest
if: ${{ github.event.inputs.ROLE != '' }}
steps:
- name: Checkout git repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: ${{ env.COLLECTION_PATH }}

- name: Validate inputs
run: |
if [[ -n "${{ env.ROLE }}" ]]; then
if [[ ! -d "${{ env.COLLECTION_PATH }}/ansible/roles/${{ env.ROLE }}" ]]; then
echo "::error::Role '${{ env.ROLE }}' not found in ansible/roles/"
exit 1
fi
if [[ ! -f "${{ env.COLLECTION_PATH }}/ansible/roles/${{ env.ROLE }}/tasks/main.yml" ]]; then
echo "::error::Role '${{ env.ROLE }}' has no tasks/main.yml"
exit 1
fi
fi

syntax-check:
needs: detect-changes
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout git repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: ${{ env.COLLECTION_PATH }}

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
cache-dependency-path: '${{ env.COLLECTION_PATH }}/${{ env.REQUIREMENTS_FILE }}'

- name: Cache Ansible collections
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.ansible/collections
key: ${{ runner.os }}-ansible-${{ hashFiles('**/requirements.yml') }}

- name: Install dependencies
run: |
python3 -m pip install -r "${{ env.COLLECTION_PATH }}/${{ env.REQUIREMENTS_FILE }}"

- name: Install galaxy dependencies
working-directory: ${{ env.COLLECTION_PATH }}/ansible
run: |
ansible-galaxy collection install -r requirements.yml --force

- name: Build and install collection locally
working-directory: ${{ env.COLLECTION_PATH }}/ansible
run: |
ansible-galaxy collection build --force
ansible-galaxy collection install ${{ env.COLLECTION_NAMESPACE }}-${{ env.COLLECTION_NAME }}-*.tar.gz -p ~/.ansible/collections --force --pre

- name: Syntax check roles
env:
ANSIBLE_CONFIG: ${{ env.COLLECTION_PATH }}/ansible/ansible.cfg
ANSIBLE_ROLES_PATH: ${{ env.COLLECTION_PATH }}/ansible/roles
TEST_ALL: ${{ needs.detect-changes.outputs.test_all }}
CHANGED_ROLES: ${{ needs.detect-changes.outputs.roles }}
SINGLE_ROLE: ${{ env.ROLE }}
run: |
set -e
FAILED=0
PASSED=0
SKIPPED=0
ROLES_DIR="${{ env.COLLECTION_PATH }}/ansible/roles"
TMPDIR=$(mktemp -d)

for role_dir in "$ROLES_DIR"/*/; do
role=$(basename "$role_dir")

# Skip roles without tasks
if [ ! -f "$role_dir/tasks/main.yml" ]; then
continue
fi

# If a single role was specified, only test that one
if [ -n "$SINGLE_ROLE" ]; then
if [ "$role" != "$SINGLE_ROLE" ]; then
continue
fi
# If not testing all, filter to changed roles
elif [ "$TEST_ALL" != "true" ] && [ -n "$CHANGED_ROLES" ]; then
if ! echo "$CHANGED_ROLES" | grep -qw "$role"; then
SKIPPED=$((SKIPPED + 1))
continue
fi
fi

echo "::group::Syntax check: $role"

# Generate temporary playbook
cat > "$TMPDIR/check_${role}.yml" <<PLAYBOOK
---
- name: Syntax check ${role}
hosts: all
gather_facts: false
tasks:
- name: Include role
ansible.builtin.include_role:
name: dreadnode.goad.${role}
PLAYBOOK

if ansible-playbook --syntax-check "$TMPDIR/check_${role}.yml"; then
echo "PASS: $role"
PASSED=$((PASSED + 1))
else
echo "::error::Syntax check failed for role: $role"
FAILED=$((FAILED + 1))
fi
echo "::endgroup::"
done

rm -rf "$TMPDIR"

echo ""
echo "=== Results ==="
echo "Passed: $PASSED"
echo "Failed: $FAILED"
echo "Skipped: $SKIPPED"

if [ "$FAILED" -gt 0 ]; then
echo "::error::$FAILED role(s) failed syntax check"
exit 1
fi

if [ "$PASSED" -eq 0 ] && [ -z "$SINGLE_ROLE" ]; then
echo "No roles were checked. This may indicate a problem with change detection."
fi
59 changes: 23 additions & 36 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,47 +1,34 @@
.vagrant/
.idea/
.venv/
.archives/
writeup/
docs/scenarios/
docs/notes.md
temp/
ansible/.venv/
ansible/collections/
ansible/test.yml
*VBoxHeadless*.log
.terraform/
.terraform.lock.hcl
*.tfstate*
tfplan
*.pem
.DS_Store
__pycache__/
*.pyc
*.tar.gz
*.log
*retry

# Ansible artifacts
.ansible/
.task/

# Build artifacts
ansible/roles/adcs_templates/files/ADCSTemplate.zip
ansible/roles/vulns_adcs_templates/files/ADCSTemplate.zip

# Scenario data (keep only tracked environments)
ad/PURPLE
ad/REDLAB
ad/EDRLAB
ad/DEMO
ad/FEDGOAD
ansible/roles/edr
ansible/edr.yml
ansible/private_data_dir/artifacts/
.vscode
ad/MINILAB

# Keys and secrets
*.pem
ad/*/providers/*/ssh_keys/*id_rsa*
ad/*/providers/*/ssh_keys/*.pub
ad/*/providers/*/extensions/*.rb
__pycache__/
*.pyc
workspace/*
todo.md
next_extensions/
/extensions/exchange/ansible/iso/resources/iso/*.ISO
docs/site/
docs/olddocs/
docs/mkdocs/site/
scripts/archives/
.task
*retry
*.log
ansible/.\\AnsiballZ_command.ps1
.ansible
ansible/roles/adcs_templates/files/ADCSTemplate.zip
ansible/roles/vulns/adcs_templates/files/ADCSTemplate.zip

# IDE / OS
.vscode
temp/
2 changes: 1 addition & 1 deletion .hooks/docsible-hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ fi
FILES_MODIFIED=0

# Process roles
for role_dir in roles/*/; do
for role_dir in ansible/roles/*/; do
[ -d "$role_dir" ] || continue

role_name=$(basename "$role_dir")
Expand Down
14 changes: 3 additions & 11 deletions .hooks/gen-arch-diagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ def analyze(self):
if role_dir.is_dir() and not role_dir.name.startswith('.'):
self.structure['roles'].append({
'name': role_dir.name,
'has_molecule': (role_dir / 'molecule').exists()
})

# Analyze plugins
Expand All @@ -45,7 +44,6 @@ def analyze(self):
if item.is_dir() and not item.name.startswith('.'):
self.structure['playbooks'].append({
'name': item.name,
'has_molecule': (item / 'molecule').exists()
})

return self.structure
Expand All @@ -65,19 +63,13 @@ def generate_mermaid(structure):
if structure['roles']:
lines.append(" Collection --> Roles[⚙️ Roles]")
for i, role in enumerate(structure['roles']):
role_label = role['name']
if role['has_molecule']:
role_label += " 🧪"
lines.append(f" Roles --> R{i}[{role_label}]")
lines.append(f" Roles --> R{i}[{role['name']}]")

# Add playbooks
if structure['playbooks']:
lines.append(" Collection --> Playbooks[📚 Playbooks]")
for i, playbook in enumerate(structure['playbooks']):
pb_label = playbook['name']
if playbook['has_molecule']:
pb_label += " 🧪"
lines.append(f" Playbooks --> PB{i}[{pb_label}]")
lines.append(f" Playbooks --> PB{i}[{playbook['name']}]")

lines.append("```")
return '\n'.join(lines)
Expand Down Expand Up @@ -127,7 +119,7 @@ def update_readme(mermaid_content):
def main():
"""Main function for pre-commit hook"""
# Analyze collection from current directory
analyzer = AnsibleCollectionAnalyzer('.')
analyzer = AnsibleCollectionAnalyzer('ansible')
structure = analyzer.analyze()

# Generate Mermaid diagram
Expand Down
6 changes: 3 additions & 3 deletions .hooks/linters/ansible-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ exclude_paths:
- ad/NHA/files/templates/
- docs/mkdocs/mkdocs.yml
- noansible_requirements.yml
- requirements_311.yml
- ansible/requirements_311.yml
- template/provider/ludus/
- playbooks.yml
- extensions/exchange/ansible/install.yml
- extensions/ws01/ansible/install.yml
- ansible/extensions/exchange/ansible/install.yml
- ansible/extensions/ws01/ansible/install.yml
- ansible/roles/onlyusers/

skip_list:
Expand Down
1 change: 1 addition & 0 deletions .hooks/linters/markdownlint.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"MD055": false,
"MD056": false,
"MD057": false,
"MD060": false,
"line-length": false,
"no-multiple-blanks": false
}
4 changes: 0 additions & 4 deletions .hooks/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
ansible-core==2.20.4
docker==7.1.0
docsible==0.8.0
molecule==26.3.0
molecule-docker==2.1.0
molecule-plugins[docker]==25.8.12
pre-commit==4.5.1
Loading
Loading