Skip to content

Commit 62dbd14

Browse files
Merge pull request #23 from vyrox-security/fix/defender-tamper-and-ci
v0.2.0 fix(sim): escape $true in high_defender_tamper + add CI smoke test
2 parents d5417d3 + c0a950e commit 62dbd14

5 files changed

Lines changed: 172 additions & 2 deletions

File tree

.github/workflows/simulator-ci.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Simulator CI
2+
3+
# Gates the public-facing simulator. simulate.sh is the script external users
4+
# run against their own ingestion deployment, so a syntax error or a broken
5+
# signer would land in their hands; CI catches both before merge. The smoke
6+
# test is hermetic (no ingestion server): it builds a real scenario payload,
7+
# signs it, and asserts the signed-request shape the webhook expects. Every
8+
# action is pinned to a full commit SHA, not a tag, so a compromised or
9+
# retagged action cannot inject code into CI.
10+
11+
on:
12+
push:
13+
branches: ["main"]
14+
pull_request:
15+
branches: ["main"]
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
shellcheck-and-smoke:
22+
runs-on: ubuntu-latest
23+
steps:
24+
- name: Checkout repository
25+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
26+
27+
- name: Syntax check (sh -n)
28+
# Catches a broken simulate.sh before any external user runs it. Also
29+
# checks the scenario scripts and the smoke test itself.
30+
run: |
31+
sh -n simulate.sh
32+
for s in scenarios/*.sh; do sh -n "$s"; done
33+
sh -n scripts/smoke.sh
34+
35+
- name: ShellCheck
36+
# Static analysis of the shell scripts. Uses the shellcheck binary
37+
# preinstalled on the ubuntu-latest runner rather than a third-party
38+
# action, so there is no extra action SHA to pin and trust.
39+
# Scoped to error severity: the scenario scripts set metadata vars
40+
# (SCENARIO_NAME, etc.) that are consumed only after simulate.sh sources
41+
# them, so shellcheck's per-file analysis flags them as unused/unassigned
42+
# (SC2034 / SC2153) - false positives across the source boundary. Real
43+
# errors still fail the build; sh -n above already covers syntax.
44+
run: |
45+
shellcheck --version
46+
shellcheck --severity=error -x simulate.sh scripts/smoke.sh
47+
shellcheck --severity=error scenarios/*.sh
48+
49+
- name: Signature-shape smoke test
50+
# Hermetic: sources simulate.sh (the BASH_SOURCE guard means main does
51+
# not fire on source), builds a scenario payload, signs it, and asserts
52+
# a 64-hex HMAC-SHA256 digest under a "sha256=" header. No server.
53+
run: bash scripts/smoke.sh

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,35 @@ All notable changes to the Vyrox attack simulator are documented here.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.0] - 2026-06-14
9+
10+
The demo-fire scenario suite plus CI for the public-facing simulator.
11+
12+
### Added
13+
- **`demo-fire.sh`** — fire alerts by SEVERITY (random pick from a pool) or by
14+
exact scenario name; `--both` (both demo tenants), `--all` (every scenario in
15+
a band), `--populate` (full LOW -> MEDIUM -> HIGH -> CRITICAL spread). Five
16+
scenarios per band, command lines chosen so Vyrox's own triage lands each
17+
alert in its band.
18+
- **`scripts/smoke.sh`** — hermetic signature-shape smoke test (no ingestion
19+
server): builds a real scenario payload, signs it, and asserts the
20+
`sha256=<64-hex>` HMAC-SHA256 wire shape the webhook expects.
21+
- **CI** (`.github/workflows/simulator-ci.yml`) — `sh -n` syntax check,
22+
ShellCheck (error severity), and the smoke test on every push/PR. Actions are
23+
pinned to a full commit SHA and the job runs with `contents: read` only.
24+
25+
### Changed
26+
- **`simulate.sh` only dispatches when executed directly** (a `BASH_SOURCE`
27+
guard), so the smoke test can source it to exercise the signer and payload
28+
builder without sending anything.
29+
30+
### Fixed
31+
- **`high_defender_tamper.sh` crashed `demo-fire.sh --populate`** with "line 31:
32+
true: unbound variable": the PowerShell literal `$true` sat inside an unquoted
33+
heredoc, so `set -u` treated it as an unset shell variable. Escaped to `\$true`
34+
so the payload carries the literal `$true`. All scenarios now build clean under
35+
`set -u`.
36+
837
## [0.1.0] - 2026-05-25
938

1039
First tagged release of the attack simulator, fire realistic, signed EDR

scenarios/high_defender_tamper.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ build_payload() {
3939
"process": {
4040
"user_name": "CORP\\\\attacker",
4141
"file_name": "powershell.exe",
42-
"command_line": "powershell Set-MpPreference -DisableRealtimeMonitoring $true",
42+
"command_line": "powershell Set-MpPreference -DisableRealtimeMonitoring \$true",
4343
"sha256": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd"
4444
},
4545
"tactic": "${SCENARIO_TACTIC}",

scripts/smoke.sh

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#!/usr/bin/env bash
2+
# =============================================================================
3+
# Vyrox Simulator smoke test (CI-05)
4+
# =============================================================================
5+
#
6+
# A fast, hermetic check that the public-facing simulator still emits a
7+
# correctly-shaped, signed request. It needs NO ingestion server: it sources
8+
# simulate.sh (which, thanks to the BASH_SOURCE guard, defines its functions
9+
# without dispatching), builds a real scenario payload, signs it the exact way
10+
# send_alert does, and asserts the signature shape the ingestion webhook
11+
# expects.
12+
#
13+
# What it proves:
14+
# 1. simulate.sh is syntactically valid (sh -n, also run in CI separately).
15+
# 2. build_payload produces valid JSON for a known scenario.
16+
# 3. build_signature produces a 64-char lowercase-hex HMAC-SHA256 digest.
17+
# 4. that digest matches an independent openssl computation (the signer is
18+
# doing real HMAC-SHA256 over the payload with the secret, not a stub).
19+
# 5. the wire header send_alert would set is "sha256=<hex>" (the prefix the
20+
# ingestion service strips), so the contract with the webhook holds.
21+
#
22+
# Exit 0 on success, non-zero (with a message) on the first failed assertion.
23+
# =============================================================================
24+
25+
set -euo pipefail
26+
27+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28+
SIM_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
29+
30+
fail() {
31+
echo "[FAIL] $*" >&2
32+
exit 1
33+
}
34+
ok() {
35+
echo "[OK] $*"
36+
}
37+
38+
# A throwaway test secret (NOT a real credential): the smoke test only checks
39+
# the signature SHAPE, it never reaches a server, so any value works.
40+
TEST_SECRET="smoke-test-secret-not-a-real-credential"
41+
TEST_TENANT="smoke-tenant"
42+
43+
# 1. Syntax check (cheap, also gives a clear message if the script is broken).
44+
sh -n "${SIM_ROOT}/simulate.sh" || fail "simulate.sh failed syntax check (sh -n)"
45+
ok "simulate.sh passes syntax check"
46+
47+
# Source the simulator. The BASH_SOURCE guard in simulate.sh means main does
48+
# NOT run on source, so this defines build_payload/build_signature/etc with no
49+
# network side effects. The scenario scripts define build_payload; source the
50+
# one we test, same as run_scenario does.
51+
# shellcheck source=/dev/null
52+
source "${SIM_ROOT}/simulate.sh"
53+
# shellcheck source=/dev/null
54+
source "${SIM_ROOT}/scenarios/mimikatz.sh"
55+
56+
# 2. build_payload produces valid JSON.
57+
payload="$(build_payload "${TEST_TENANT}")"
58+
[[ -n "${payload}" ]] || fail "build_payload returned an empty payload"
59+
echo "${payload}" | python3 -m json.tool >/dev/null 2>&1 \
60+
|| fail "build_payload did not produce valid JSON"
61+
ok "build_payload emits valid JSON for the mimikatz scenario"
62+
63+
# 3 + 4. build_signature produces a 64-char lowercase-hex digest that matches
64+
# an independent HMAC-SHA256 computation over the same bytes.
65+
signature="$(build_signature "${payload}" "${TEST_SECRET}")"
66+
[[ "${signature}" =~ ^[0-9a-f]{64}$ ]] \
67+
|| fail "build_signature output is not a 64-char lowercase-hex digest: '${signature}'"
68+
ok "build_signature emits a 64-char hex HMAC-SHA256 digest"
69+
70+
expected="$(printf '%s' "${payload}" | openssl dgst -sha256 -hmac "${TEST_SECRET}" | sed 's/^.* //')"
71+
[[ "${signature}" == "${expected}" ]] \
72+
|| fail "build_signature digest does not match an independent openssl HMAC-SHA256"
73+
ok "signature matches an independent HMAC-SHA256 computation"
74+
75+
# 5. The wire header send_alert sets is "sha256=<hex>": the prefix the
76+
# ingestion webhook strips before verifying.
77+
header_value="sha256=${signature}"
78+
[[ "${header_value}" =~ ^sha256=[0-9a-f]{64}$ ]] \
79+
|| fail "X-Vyrox-Signature header is not 'sha256=<64-hex>': '${header_value}'"
80+
ok "X-Vyrox-Signature header has the expected 'sha256=<hex>' shape"
81+
82+
echo "[PASS] simulator smoke test: payload + signed-request shape is correct"

simulate.sh

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,4 +355,10 @@ main() {
355355
fi
356356
}
357357

358-
main "$@"
358+
# Only dispatch when executed directly. When the script is SOURCED (the CI
359+
# smoke test sources it to exercise build_payload / build_signature without a
360+
# live ingestion server), the functions are defined but main does not fire, so
361+
# sourcing has no side effects and sends nothing.
362+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
363+
main "$@"
364+
fi

0 commit comments

Comments
 (0)