diff --git a/.github/workflows/build-and-push-templates.yaml b/.github/workflows/build-and-push-templates.yaml index fd2e24db..20ed2805 100644 --- a/.github/workflows/build-and-push-templates.yaml +++ b/.github/workflows/build-and-push-templates.yaml @@ -30,7 +30,7 @@ env: PYTHON_VERSION: 3.13.7 TASK_VERSION: 3.45.5 TASK_X_REMOTE_TASKFILES: 1 - WARPGATE_VERSION: "v4.6.0" + WARPGATE_VERSION: "v4.7.0" jobs: discover-templates: @@ -192,15 +192,16 @@ jobs: DEPENDENCY_MAP="{}" PLATFORMS_MAP="{}" ANSIBLE_MAP="{}" - AMI_TEMPLATES=() + NON_CONTAINER_TEMPLATES=() for template_dir in "$GITHUB_WORKSPACE"/warpgate-templates/templates/*/; do if [ -f "${template_dir}warpgate.yaml" ]; then template_name=$(basename "$template_dir") - # Skip AMI-only templates - they cannot be built as containers - if grep -q "type: ami" "${template_dir}warpgate.yaml" && ! grep -q "type: container" "${template_dir}warpgate.yaml"; then - echo " $template_name: SKIPPED (AMI-only template)" - AMI_TEMPLATES+=("$template_name") + # Skip templates without a container target (e.g. AMI-only, Azure-only) + # — this workflow builds and pushes container images. + if ! grep -q "type: container" "${template_dir}warpgate.yaml"; then + echo " $template_name: SKIPPED (no container target)" + NON_CONTAINER_TEMPLATES+=("$template_name") continue fi @@ -282,13 +283,13 @@ jobs: ] }') - # Remove AMI-only templates from matrix (they have no dependency/platform maps) - if [ ${#AMI_TEMPLATES[@]} -gt 0 ]; then - AMI_JSON=$(printf '%s\n' "${AMI_TEMPLATES[@]}" | jq -R . | jq -s .) - FULL_MATRIX=$(echo "$FULL_MATRIX" | jq --argjson ami "$AMI_JSON" '{ - include: [.include[] | select(.name as $n | $ami | index($n) | not)] + # Remove non-container templates from matrix (they have no dependency/platform maps) + if [ ${#NON_CONTAINER_TEMPLATES[@]} -gt 0 ]; then + EXCLUDE_JSON=$(printf '%s\n' "${NON_CONTAINER_TEMPLATES[@]}" | jq -R . | jq -s .) + FULL_MATRIX=$(echo "$FULL_MATRIX" | jq --argjson exclude "$EXCLUDE_JSON" '{ + include: [.include[] | select(.name as $n | $exclude | index($n) | not)] }') - echo "Excluded AMI-only templates from container build matrix: ${AMI_TEMPLATES[*]}" + echo "Excluded non-container templates from container build matrix: ${NON_CONTAINER_TEMPLATES[*]}" fi # Apply template filter if provided via workflow_dispatch diff --git a/.github/workflows/test-template-builds.yaml b/.github/workflows/test-template-builds.yaml index 99a15ca1..e70c0453 100644 --- a/.github/workflows/test-template-builds.yaml +++ b/.github/workflows/test-template-builds.yaml @@ -24,7 +24,7 @@ concurrency: env: DEBIAN_FRONTEND: noninteractive PYTHON_VERSION: "3.13.7" - WARPGATE_VERSION: "v4.6.0" + WARPGATE_VERSION: "v4.7.0" jobs: detect-changes: @@ -100,9 +100,10 @@ jobs: continue fi - # Skip AMI templates - they require AWS credentials and cannot be tested in CI - if grep -qE '^\s+- type: ami' "${template_dir}/warpgate.yaml"; then - echo "Skipping $template_name: AMI target type cannot be tested in CI" + # Skip templates without a container target (AMI-only, Azure-only, etc.) + # — this workflow tests container builds in CI. + if ! grep -q "type: container" "${template_dir}/warpgate.yaml"; then + echo "Skipping $template_name: no container target (only container builds are tested in CI)" continue fi diff --git a/.github/workflows/validate-templates.yaml b/.github/workflows/validate-templates.yaml index 35bed776..9eb7b852 100644 --- a/.github/workflows/validate-templates.yaml +++ b/.github/workflows/validate-templates.yaml @@ -21,7 +21,7 @@ on: workflow_dispatch: env: - WARPGATE_VERSION: "v4.6.0" + WARPGATE_VERSION: "v4.7.0" PYTHON_VERSION: "3.13.7" TASK_VERSION: "3.45.5" TASK_X_REMOTE_TASKFILES: 1 @@ -103,6 +103,17 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Validate templates with warpgate (syntax-only) + # warpgate validate resolves ${VAR} references before checking required + # fields, so placeholders are needed for env-driven values (Azure target + # IDs, GITHUB_TOKEN). Real values are supplied at build time. + env: + GITHUB_TOKEN: placeholder + AZURE_SUBSCRIPTION_ID: 00000000-0000-0000-0000-000000000000 + AZURE_LOCATION: centralus + AZURE_RESOURCE_GROUP: placeholder-rg + AZURE_GALLERY_NAME: placeholder-gallery + AZURE_IDENTITY_ID: /subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/placeholder-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/placeholder-uami + AZURE_VM_SIZE: Standard_D4s_v3 run: | failed=0 while IFS= read -r template; do @@ -150,11 +161,14 @@ jobs: fi - name: Validate YAML syntax + # Reuse the project-wide yamllint config (.hooks/linters/yamllint.yaml) + # so this step matches what pre-commit runs locally — keeps shell-heavy + # provisioner inlines from drowning the log in line-length warnings. run: | pip install yamllint find warpgate-templates -name '*.yaml' -o -name '*.yml' | while read -r file; do echo "Checking YAML syntax: $file" - yamllint -d "{extends: relaxed, rules: {line-length: {max: 120}}}" "$file" + yamllint -c .hooks/linters/yamllint.yaml "$file" done - name: Validate against JSON schema @@ -169,8 +183,10 @@ jobs: from pathlib import Path from jsonschema import Draft7Validator - # Download the schema - schema_url = "https://raw.githubusercontent.com/cowdogmoo/warpgate/v4.4.0/schema/warpgate-template.json" + # Download the schema (version pinned to the warpgate binary version + # from the workflow env so schema and validator stay in lockstep) + warpgate_version = "${{ env.WARPGATE_VERSION }}" + schema_url = f"https://raw.githubusercontent.com/cowdogmoo/warpgate/{warpgate_version}/schema/warpgate-template.json" print(f"Downloading schema from: {schema_url}") try: diff --git a/warpgate-templates/templates/ares-golden-azure/README.md b/warpgate-templates/templates/ares-golden-azure/README.md new file mode 100644 index 00000000..fcfc00f2 --- /dev/null +++ b/warpgate-templates/templates/ares-golden-azure/README.md @@ -0,0 +1,51 @@ +# ares-golden-azure + +Azure variant of the Ares golden image. Builds a Kali Linux image via Azure +VM Image Builder and publishes a version into a Compute Gallery, with feature +parity against the AWS `ares-golden-image` AMI. + +Ships the same red-team toolchain installed by +`ansible/playbooks/ares/goad_attack_box.yml`: + +- recon, credential access, privilege escalation +- password cracking (hashcat from source, GPU-accelerated) +- lateral movement, ACL abuse, coercion +- Alloy telemetry agent +- NVIDIA driver + CUDA toolkit for T4 GPU acceleration + +## Prerequisites + +The template's `targets[].azure` fields are parameterized via environment +variables so the same template works across subscriptions and environments. +The values below are placeholders — substitute your own. + +Provisioned manually (one-time): + +- An Azure subscription (`${AZURE_SUBSCRIPTION_ID}`) +- A resource group (`${AZURE_RESOURCE_GROUP}`) in your chosen region + (`${AZURE_LOCATION}`, e.g. `centralus`) +- A Compute Gallery (`${AZURE_GALLERY_NAME}`) +- Image definition `ares-golden-azure` (Linux, Generalized, HyperV V2, + publisher=`dreadnode`, offer=`ares`, sku=`golden`) +- A user-assigned managed identity (`${AZURE_IDENTITY_ID}` — full resource ID) + with Contributor on the resource group +- Quota for the chosen `${AZURE_VM_SIZE}` in `${AZURE_LOCATION}` + (e.g. `Standard_NC4as_T4_v3` for T4 GPU, `Standard_D4s_v3` for CPU-only) +- Kali Marketplace terms accepted on the subscription: + `az vm image terms accept --publisher kali-linux --offer kali --plan kali-2026-1` + +## Build + +Export the required env vars, then build: + +```bash +export AZURE_SUBSCRIPTION_ID= +export AZURE_LOCATION=centralus +export AZURE_RESOURCE_GROUP= +export AZURE_GALLERY_NAME= +export AZURE_IDENTITY_ID=/subscriptions//resourcegroups//providers/Microsoft.ManagedIdentity/userAssignedIdentities/ +export AZURE_VM_SIZE=Standard_NC4as_T4_v3 +export GITHUB_TOKEN= + +warpgate build path/to/ares-golden-azure --target azure +``` diff --git a/warpgate-templates/templates/ares-golden-azure/warpgate.yaml b/warpgate-templates/templates/ares-golden-azure/warpgate.yaml new file mode 100644 index 00000000..121a31ca --- /dev/null +++ b/warpgate-templates/templates/ares-golden-azure/warpgate.yaml @@ -0,0 +1,100 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/cowdogmoo/warpgate/main/schema/warpgate-template.json +metadata: + name: ares-golden-azure + version: 1.0.0 + description: Azure variant of the Ares golden image with all red team tools - recon, credential access, privesc, cracking, lateral movement, ACL abuse, and coercion + author: Dreadnode + license: MIT + tags: + - ares + - golden-image + - azure + - red-team + - reconnaissance + - credential-access + - privilege-escalation + - password-cracking + - lateral-movement + - acl + - coercion + requires: + warpgate: '>=1.0.0' + +name: ares-golden-azure +version: latest + +base: + image: kalilinux/kali-rolling@sha256:dddc31e0f4bc57b4b91e9027762544506bf91c7cdd7ff52104daaa4449b4c726 + +provisioners: + # Install pipx + Ansible, then fetch the nimbus_range collection on the build VM. + # We re-clone in shell rather than using warpgate's `sources` + `type: file` + # pattern (see ares-golden-image) because Azure Image Builder expands `type: file` + # into one customizer per file and times out on the 2000+ file ansible/ tree. + # Token is passed via a credential helper so it never appears in the clone URL + # or AIB customizer logs; ref tracks the AMI variant. + - type: shell + inline: + - apt-get update + - apt-get install -y --no-install-recommends ca-certificates git procps sudo python3-apt python3-pip python3-venv pipx + - 'sed -i ''s|^PATH="|PATH="/root/.local/bin:/root/.cargo/bin:|'' /etc/environment || echo ''PATH="/root/.local/bin:/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"'' > /etc/environment' + - pipx install --force uv + - pipx install --force ansible-core + - pipx ensurepath + - GITHUB_TOKEN=${GITHUB_TOKEN} git -c 'credential.helper=!f() { echo username=x-access-token; echo password=$GITHUB_TOKEN; }; f' clone --depth 1 --branch feat/more-attack-cov https://github.com/dreadnode/ares.git /tmp/nimbus_range + - mkdir -p /root/.ansible/collections/ansible_collections/dreadnode/nimbus_range + - cp -r /tmp/nimbus_range/ansible/. /root/.ansible/collections/ansible_collections/dreadnode/nimbus_range/ + - rm -rf /tmp/nimbus_range + + # Attack Box - all red team tools + Alloy telemetry + # NOTE: Using shell instead of ansible provisioner because the playbook + # exceeds Azure VM Image Builder's customizer length limit when inlined. + - type: shell + inline: + - PATH=/root/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ansible-galaxy collection install -r /root/.ansible/collections/ansible_collections/dreadnode/nimbus_range/requirements.yml --force + - HOME=/root ANSIBLE_REMOTE_TMP=/tmp/ansible-tmp-$USER PATH=/root/.local/bin:/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ansible-playbook /root/.ansible/collections/ansible_collections/dreadnode/nimbus_range/playbooks/ares/goad_attack_box.yml -i localhost, -c local -e ansible_shell_executable=/bin/bash -e ansible_python_interpreter=/usr/bin/python3 -e cracking_tools_gpu_support=true -e cracking_tools_hashcat_from_source=true -e cracking_tools_nvidia_opencl_icd=true + + # NVIDIA GPU drivers + CUDA toolkit for hashcat GPU acceleration on NCas T4 v3. + # Kernel headers + dkms are required so the nvidia module builds for the + # running kernel. The image then works on GPU instances without manual driver + # setup. nvidia-smi may not be available during image build if no GPU is attached. + - type: shell + inline: + - apt-get update + - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends linux-headers-$(uname -r) dkms nvidia-driver nvidia-cuda-toolkit firmware-misc-nonfree + - nvidia-smi || echo "nvidia-smi not available during image build (expected if no GPU attached)" + + # Cleanup + - type: shell + inline: + - apt-get clean + - rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + - echo "Ares golden azure build completed successfully" + +targets: + - type: azure + os_type: Linux + subscription_id: ${AZURE_SUBSCRIPTION_ID} + location: ${AZURE_LOCATION} + resource_group: ${AZURE_RESOURCE_GROUP} + gallery: ${AZURE_GALLERY_NAME} + gallery_image_definition: ares-golden-azure + identity_id: ${AZURE_IDENTITY_ID} + vm_size: ${AZURE_VM_SIZE} + # For GPU: set AZURE_VM_SIZE=Standard_NC4as_T4_v3 + # For CPU-only test builds: set AZURE_VM_SIZE=Standard_D4s_v3 + source_image: + marketplace: + publisher: kali-linux + offer: kali + sku: kali-2026-1 + version: latest + plan: + name: kali-2026-1 + product: kali + publisher: kali-linux + image_tags: + Project: ares + Role: RedTeamAttackBox + ManagedBy: warpgate + Tools: recon,credential-access,privesc,cracker,lateral-movement,acl-abuse,coercion diff --git a/warpgate-templates/templates/ares-golden-image/warpgate.yaml b/warpgate-templates/templates/ares-golden-image/warpgate.yaml index 6f820921..d56af8af 100644 --- a/warpgate-templates/templates/ares-golden-image/warpgate.yaml +++ b/warpgate-templates/templates/ares-golden-image/warpgate.yaml @@ -33,12 +33,12 @@ base: most_recent: true sources: - - name: nimbus_range - git: - repository: https://github.com/dreadnode/ansible-collection-nimbus_range.git - depth: 1 - auth: - token: ${GITHUB_TOKEN} + # Use the in-repo ansible/ tree directly so builds match the working copy + # (no GITHUB_TOKEN, no branch ref drift). Path is relative to this template's + # directory; requires warpgate >= v4.7.0 (local source type). + - name: ares + local: + path: ../../../ansible provisioners: # Install pipx and Ansible @@ -51,15 +51,17 @@ provisioners: - pipx install --force ansible-core - pipx ensurepath - # Copy ansible collection from source (cloned securely by warpgate without embedding token in shell commands) + # Copy ansible collection from the local source (the in-repo ansible/ tree). + # The destination keeps the `nimbus_range` name because the ansible collection is published as + # `dreadnode.nimbus_range`; subsequent steps install it under that namespace. - type: file - source: ${sources.nimbus_range} + source: ${sources.ares} destination: /tmp/nimbus_range - type: shell inline: - mkdir -p /root/.ansible/collections/ansible_collections/dreadnode/nimbus_range - - cp -r /tmp/nimbus_range/* /root/.ansible/collections/ansible_collections/dreadnode/nimbus_range/ + - cp -r /tmp/nimbus_range/. /root/.ansible/collections/ansible_collections/dreadnode/nimbus_range/ - rm -rf /tmp/nimbus_range # Install NVIDIA drivers for GPU-accelerated hashcat on g4dn (T4 GPU)