diff --git a/.github/workflows/e2e-repl-test.yml b/.github/workflows/e2e-repl-test.yml new file mode 100644 index 000000000..669e105de --- /dev/null +++ b/.github/workflows/e2e-repl-test.yml @@ -0,0 +1,92 @@ +name: E2E REPL Test ๐Ÿงช + +on: + workflow_dispatch: ~ + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + e2e_repl_test: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v5 + - name: โšก Setup Golang + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + + - name: Setup Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: '1.91.1' + default: true + profile: minimal + components: rustfmt, clippy + + - name: โšก Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: tavern/internal/www/package-lock.json + + - name: ๐Ÿ“ฆ Install System Dependencies + run: | + sudo apt-get update + sudo apt-get install -y protobuf-compiler libssl-dev + - name: ๐Ÿ”จ Build Tavern + run: | + go mod download + go build -v -o tavern_bin ./tavern + - name: ๐Ÿš€ Run Tavern + env: + HTTP_LISTEN_ADDR: ":8000" + run: | + ./tavern_bin & + echo "Waiting for Tavern to start..." + # Wait for port 8000 + timeout 30 sh -c 'until nc -z $0 $1; do sleep 1; done' localhost 8000 + - name: ๐Ÿค– Run Agent + working-directory: implants/imixv2 + env: + IMIX_CALLBACK_URI: "http://localhost:8000" + IMIX_CALLBACK_INTERVAL: 1 + run: | + # Fetch the pubkey and verify it's not empty + PUBKEY=$(curl -s http://localhost:8000/status | jq -r .Pubkey) + if [ -z "$PUBKEY" ] || [ "$PUBKEY" == "null" ]; then + echo "Error: Could not fetch Pubkey from Tavern" + exit 1 + fi + export IMIX_SERVER_PUBKEY=$PUBKEY + echo "Got pubkey: $IMIX_SERVER_PUBKEY" + + echo "Building imixv2..." + cargo build --bin imixv2 --target-dir ./build + # Run agent and pipe logs to a file + ./build/debug/imixv2 > agent.log 2>&1 & + + # Give the agent a moment to perform the initial handshake + echo "Agent started. Waiting for initial callback..." + sleep 5 + - name: ๐ŸŽญ Install Playwright + working-directory: tests/e2e + run: | + npm ci + npx playwright install --with-deps chromium + - name: ๐Ÿงช Run E2E Tests + working-directory: tests/e2e + run: | + npx playwright test + - name: ๐Ÿ“‚ Upload Service Logs + if: always() # Runs even if tests fail + uses: actions/upload-artifact@v4 + with: + name: e2e-logs + path: | + tavern.log + implants/imixv2/agent.log diff --git a/.gitignore b/.gitignore index 2357637ca..bad2e4f26 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,10 @@ # Build artifacts dist/** +# Playwright E2E Test Results +tests/e2e/test-results/** +tests/e2e/playwright-report/** + # Binaries for programs and plugins *.exe *.exe~ diff --git a/tests/e2e/package-lock.json b/tests/e2e/package-lock.json new file mode 100644 index 000000000..ef2898f7b --- /dev/null +++ b/tests/e2e/package-lock.json @@ -0,0 +1,96 @@ +{ + "name": "e2e-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "e2e-tests", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "^1.40.0", + "@types/node": "^18.0.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/tests/e2e/tests/e2e.spec.ts b/tests/e2e/tests/e2e.spec.ts index 9433e0901..bfa6e6d9a 100644 --- a/tests/e2e/tests/e2e.spec.ts +++ b/tests/e2e/tests/e2e.spec.ts @@ -1,26 +1,33 @@ import { test, expect } from '@playwright/test'; test('End-to-end reverse shell repl test', async ({ page }) => { - // 1. Connect to tavern's UI using playwright at http://127.0.0.1:8000/createQuest + // Connect to tavern's UI using playwright at http://127.0.0.1:8000/createQuest console.log('Navigating to /createQuest'); await page.goto('/createQuest'); - // 2. Select the only visible beacon and click "continue" + // Select the only visible beacon and click "continue" console.log('Waiting for beacons to load'); await expect(page.getByText('Loading beacons...')).toBeHidden({ timeout: 15000 }); // Select the checkbox. Using force: true because Chakra UI hides the actual input. + // Define the locator for the beacon checkboxes + const beacons = page.locator('.chakra-card input[type="checkbox"]'); + + // Assert that exactly one beacon exists + await expect(beacons).toHaveCount(1); + + // Select the beacon console.log('Selecting beacon'); - await page.locator('input[type="checkbox"]').first().check({ force: true }); + await beacons.first().check({ force: true }); // Click Continue console.log('Clicking Continue (Beacon)'); await page.getByRole('button', { name: 'Continue' }).click(); - // 3. Select the "Reverse Shell Repl" tome and click "continue" + // 3. Select the "Reverse Shell REPL" tome and click "continue" console.log('Selecting Tome'); await expect(page.getByText('Loading tomes...')).toBeHidden(); - await page.getByText('Reverse Shell Repl').click(); + await page.getByText('Reverse Shell REPL').click(); console.log('Clicking Continue (Tome)'); await page.getByRole('button', { name: 'Continue' }).click(); @@ -52,7 +59,7 @@ test('End-to-end reverse shell repl test', async ({ page }) => { await expect(page.locator('#terminal')).toBeVisible({ timeout: 15000 }); // Focus the terminal (clicking it helps ensure focus) - await page.locator('.xterm-cursor-layer').click(); + await page.locator('#terminal').click(); console.log('Sending command'); // Type something.