From c0a950e0fd49c72477b44cc042954196ea2e087c Mon Sep 17 00:00:00 2001 From: keirsalterego Date: Sun, 14 Jun 2026 16:28:49 +0530 Subject: [PATCH] fix(sim): escape $true in high_defender_tamper + add CI smoke test high_defender_tamper.sh crashed demo-fire.sh --populate with "line 31: true: unbound variable": the PowerShell literal $true sat inside an unquoted heredoc, so set -u treated it as an unset shell variable. Escaped to \$true so the payload carries the literal $true. All scenarios now build clean under set -u. Also adds CI for the public-facing simulator: - .github/workflows/simulator-ci.yml: sh -n + ShellCheck + smoke on push/PR; actions pinned to a full SHA, job runs contents:read only. - scripts/smoke.sh: hermetic signature-shape test (no ingestion server). - simulate.sh: BASH_SOURCE guard so sourcing (the smoke test) does not dispatch. Bump 0.1.0 -> 0.2.0 (also documents the demo-fire suite shipped in #21). --- .github/workflows/simulator-ci.yml | 53 +++++++++++++++++++ CHANGELOG.md | 29 +++++++++++ scenarios/high_defender_tamper.sh | 2 +- scripts/smoke.sh | 82 ++++++++++++++++++++++++++++++ simulate.sh | 8 ++- 5 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/simulator-ci.yml create mode 100755 scripts/smoke.sh diff --git a/.github/workflows/simulator-ci.yml b/.github/workflows/simulator-ci.yml new file mode 100644 index 0000000..78bcac3 --- /dev/null +++ b/.github/workflows/simulator-ci.yml @@ -0,0 +1,53 @@ +name: Simulator CI + +# Gates the public-facing simulator. simulate.sh is the script external users +# run against their own ingestion deployment, so a syntax error or a broken +# signer would land in their hands; CI catches both before merge. The smoke +# test is hermetic (no ingestion server): it builds a real scenario payload, +# signs it, and asserts the signed-request shape the webhook expects. Every +# action is pinned to a full commit SHA, not a tag, so a compromised or +# retagged action cannot inject code into CI. + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +permissions: + contents: read + +jobs: + shellcheck-and-smoke: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Syntax check (sh -n) + # Catches a broken simulate.sh before any external user runs it. Also + # checks the scenario scripts and the smoke test itself. + run: | + sh -n simulate.sh + for s in scenarios/*.sh; do sh -n "$s"; done + sh -n scripts/smoke.sh + + - name: ShellCheck + # Static analysis of the shell scripts. Uses the shellcheck binary + # preinstalled on the ubuntu-latest runner rather than a third-party + # action, so there is no extra action SHA to pin and trust. + # Scoped to error severity: the scenario scripts set metadata vars + # (SCENARIO_NAME, etc.) that are consumed only after simulate.sh sources + # them, so shellcheck's per-file analysis flags them as unused/unassigned + # (SC2034 / SC2153) - false positives across the source boundary. Real + # errors still fail the build; sh -n above already covers syntax. + run: | + shellcheck --version + shellcheck --severity=error -x simulate.sh scripts/smoke.sh + shellcheck --severity=error scenarios/*.sh + + - name: Signature-shape smoke test + # Hermetic: sources simulate.sh (the BASH_SOURCE guard means main does + # not fire on source), builds a scenario payload, signs it, and asserts + # a 64-hex HMAC-SHA256 digest under a "sha256=" header. No server. + run: bash scripts/smoke.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c6083c..b344f96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ All notable changes to the Vyrox attack simulator are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.0] - 2026-06-14 + +The demo-fire scenario suite plus CI for the public-facing simulator. + +### Added +- **`demo-fire.sh`** — fire alerts by SEVERITY (random pick from a pool) or by + exact scenario name; `--both` (both demo tenants), `--all` (every scenario in + a band), `--populate` (full LOW -> MEDIUM -> HIGH -> CRITICAL spread). Five + scenarios per band, command lines chosen so Vyrox's own triage lands each + alert in its band. +- **`scripts/smoke.sh`** — hermetic signature-shape smoke test (no ingestion + server): builds a real scenario payload, signs it, and asserts the + `sha256=<64-hex>` HMAC-SHA256 wire shape the webhook expects. +- **CI** (`.github/workflows/simulator-ci.yml`) — `sh -n` syntax check, + ShellCheck (error severity), and the smoke test on every push/PR. Actions are + pinned to a full commit SHA and the job runs with `contents: read` only. + +### Changed +- **`simulate.sh` only dispatches when executed directly** (a `BASH_SOURCE` + guard), so the smoke test can source it to exercise the signer and payload + builder without sending anything. + +### Fixed +- **`high_defender_tamper.sh` crashed `demo-fire.sh --populate`** with "line 31: + true: unbound variable": the PowerShell literal `$true` sat inside an unquoted + heredoc, so `set -u` treated it as an unset shell variable. Escaped to `\$true` + so the payload carries the literal `$true`. All scenarios now build clean under + `set -u`. + ## [0.1.0] - 2026-05-25 First tagged release of the attack simulator, fire realistic, signed EDR diff --git a/scenarios/high_defender_tamper.sh b/scenarios/high_defender_tamper.sh index 7e3ad16..c8cd02e 100755 --- a/scenarios/high_defender_tamper.sh +++ b/scenarios/high_defender_tamper.sh @@ -39,7 +39,7 @@ build_payload() { "process": { "user_name": "CORP\\\\attacker", "file_name": "powershell.exe", - "command_line": "powershell Set-MpPreference -DisableRealtimeMonitoring $true", + "command_line": "powershell Set-MpPreference -DisableRealtimeMonitoring \$true", "sha256": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd" }, "tactic": "${SCENARIO_TACTIC}", diff --git a/scripts/smoke.sh b/scripts/smoke.sh new file mode 100755 index 0000000..3bdbd76 --- /dev/null +++ b/scripts/smoke.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# ============================================================================= +# Vyrox Simulator smoke test (CI-05) +# ============================================================================= +# +# A fast, hermetic check that the public-facing simulator still emits a +# correctly-shaped, signed request. It needs NO ingestion server: it sources +# simulate.sh (which, thanks to the BASH_SOURCE guard, defines its functions +# without dispatching), builds a real scenario payload, signs it the exact way +# send_alert does, and asserts the signature shape the ingestion webhook +# expects. +# +# What it proves: +# 1. simulate.sh is syntactically valid (sh -n, also run in CI separately). +# 2. build_payload produces valid JSON for a known scenario. +# 3. build_signature produces a 64-char lowercase-hex HMAC-SHA256 digest. +# 4. that digest matches an independent openssl computation (the signer is +# doing real HMAC-SHA256 over the payload with the secret, not a stub). +# 5. the wire header send_alert would set is "sha256=" (the prefix the +# ingestion service strips), so the contract with the webhook holds. +# +# Exit 0 on success, non-zero (with a message) on the first failed assertion. +# ============================================================================= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SIM_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +fail() { + echo "[FAIL] $*" >&2 + exit 1 +} +ok() { + echo "[OK] $*" +} + +# A throwaway test secret (NOT a real credential): the smoke test only checks +# the signature SHAPE, it never reaches a server, so any value works. +TEST_SECRET="smoke-test-secret-not-a-real-credential" +TEST_TENANT="smoke-tenant" + +# 1. Syntax check (cheap, also gives a clear message if the script is broken). +sh -n "${SIM_ROOT}/simulate.sh" || fail "simulate.sh failed syntax check (sh -n)" +ok "simulate.sh passes syntax check" + +# Source the simulator. The BASH_SOURCE guard in simulate.sh means main does +# NOT run on source, so this defines build_payload/build_signature/etc with no +# network side effects. The scenario scripts define build_payload; source the +# one we test, same as run_scenario does. +# shellcheck source=/dev/null +source "${SIM_ROOT}/simulate.sh" +# shellcheck source=/dev/null +source "${SIM_ROOT}/scenarios/mimikatz.sh" + +# 2. build_payload produces valid JSON. +payload="$(build_payload "${TEST_TENANT}")" +[[ -n "${payload}" ]] || fail "build_payload returned an empty payload" +echo "${payload}" | python3 -m json.tool >/dev/null 2>&1 \ + || fail "build_payload did not produce valid JSON" +ok "build_payload emits valid JSON for the mimikatz scenario" + +# 3 + 4. build_signature produces a 64-char lowercase-hex digest that matches +# an independent HMAC-SHA256 computation over the same bytes. +signature="$(build_signature "${payload}" "${TEST_SECRET}")" +[[ "${signature}" =~ ^[0-9a-f]{64}$ ]] \ + || fail "build_signature output is not a 64-char lowercase-hex digest: '${signature}'" +ok "build_signature emits a 64-char hex HMAC-SHA256 digest" + +expected="$(printf '%s' "${payload}" | openssl dgst -sha256 -hmac "${TEST_SECRET}" | sed 's/^.* //')" +[[ "${signature}" == "${expected}" ]] \ + || fail "build_signature digest does not match an independent openssl HMAC-SHA256" +ok "signature matches an independent HMAC-SHA256 computation" + +# 5. The wire header send_alert sets is "sha256=": the prefix the +# ingestion webhook strips before verifying. +header_value="sha256=${signature}" +[[ "${header_value}" =~ ^sha256=[0-9a-f]{64}$ ]] \ + || fail "X-Vyrox-Signature header is not 'sha256=<64-hex>': '${header_value}'" +ok "X-Vyrox-Signature header has the expected 'sha256=' shape" + +echo "[PASS] simulator smoke test: payload + signed-request shape is correct" diff --git a/simulate.sh b/simulate.sh index 6976ca9..1f49ba8 100755 --- a/simulate.sh +++ b/simulate.sh @@ -355,4 +355,10 @@ main() { fi } -main "$@" +# Only dispatch when executed directly. When the script is SOURCED (the CI +# smoke test sources it to exercise build_payload / build_signature without a +# live ingestion server), the functions are defined but main does not fire, so +# sourcing has no side effects and sends nothing. +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi