Merge pull request #1 from Andy7475/feature/cloud-translate
Feature/cloud translate
This commit is contained in:
commit
db32c05d93
3
.env.template
Normal file
3
.env.template
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# CyberChef Cloud Operations Tests
|
||||||
|
# Copy this file to `.env` and fill in your valid GCP API Key if you want to run the Nightwatch browser tests against live Cloud APIs.
|
||||||
|
CYBERCHEF_GCP_TEST_KEY="YOUR_API_KEY_HERE"
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -13,3 +13,4 @@ src/node/index.mjs
|
|||||||
**/*.DS_Store
|
**/*.DS_Store
|
||||||
tests/browser/output/*
|
tests/browser/output/*
|
||||||
.node-version
|
.node-version
|
||||||
|
.env
|
||||||
|
|||||||
54
docs/GCloudAuthenticationArchitecture.md
Normal file
54
docs/GCloudAuthenticationArchitecture.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Google Cloud Authentication Architecture
|
||||||
|
|
||||||
|
While the [Google Cloud Setup Guide](GoogleCloudSetup.md) covers the step-by-step instructions for extracting your API credentials, this document outlines the architectural decisions, pitfalls, and lessons learned when authenticating from CyberChef's client-side environment to Google Cloud APIs.
|
||||||
|
|
||||||
|
## The CyberChef Context
|
||||||
|
CyberChef operates entirely within the browser. There is no middle-tier backend server acting as a proxy. This means that any REST request to a Cloud API originates directly from the user's browser via `fetch()` from a Web Worker.
|
||||||
|
|
||||||
|
This architecture has three major implications for authentication:
|
||||||
|
|
||||||
|
### 1. Browser-Side Secret Exposure
|
||||||
|
Any secret embedded in a CyberChef recipe URL or baked into an operation is visible to the browser, the network layer, and anyone who receives the recipe link.
|
||||||
|
|
||||||
|
* **API Keys**: An API Key is a static secret. If you use one, you **must** apply HTTP Referrer restrictions in the Google Cloud Console (as detailed in the Setup Guide) to ensure it can only be used from your specific CyberChef domain (e.g., `http://localhost:8080/`).
|
||||||
|
* **OAuth Tokens**: A temporary OAuth Token (from `gcloud auth print-access-token`) is generally preferred because it automatically expires (usually in 1 hour), mitigating the risk of credential leakage.
|
||||||
|
|
||||||
|
### 2. The Application Default Credentials (ADC) Quota Project Pitfall
|
||||||
|
When you use a short-lived OAuth token generated by an End User account (e.g., via `gcloud auth login`), the token identifies *you*, but Google occasionally needs to know which project to bill for the specific API usage.
|
||||||
|
|
||||||
|
If you attempt to call certain APIs (like `translate.googleapis.com`) using an End User OAuth Token, Google Cloud will throw a 403 error:
|
||||||
|
> *"Your application is authenticating by using local Application Default Credentials. The translate.googleapis.com API requires a quota project, which is not set by default."*
|
||||||
|
|
||||||
|
#### The Solution:
|
||||||
|
CyberChef cloud operations that accept an OAuth Token must implement an optional **Quota Project** input argument.
|
||||||
|
Under the hood, this argument must be injected into the `fetch()` request as the `x-goog-user-project` HTTP header:
|
||||||
|
```javascript
|
||||||
|
headers.set("Authorization", `Bearer ${authString}`);
|
||||||
|
if (quotaProject) {
|
||||||
|
headers.set("x-goog-user-project", quotaProject); // Injects the billing bound project
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*Note: You configure your local `gcloud` environment's default quota project using `gcloud auth application-default set-quota-project <YOUR_PROJECT_ID>`.*
|
||||||
|
|
||||||
|
### 3. UI Argument Masking (The `toggleString` Pitfall)
|
||||||
|
To prevent your API Key or Token from being displayed in plain text within CyberChef's UI, Cloud operations should use the `toggleString` data type for the authentication argument. This renders the input cleanly and masks the text.
|
||||||
|
|
||||||
|
#### The Pitfall:
|
||||||
|
When converting a CyberChef argument from a standard `string` to a `toggleString`, the underlying payload structure changes. The Web Worker will no longer receive a raw string; it will receive an object:
|
||||||
|
```javascript
|
||||||
|
// A normal string returns: "AIzaSy..."
|
||||||
|
// A toggleString returns: { "string": "AIzaSy...", "option": "UTF8" }
|
||||||
|
```
|
||||||
|
If an operation's `run()` function expects a string and does not explicitly unpack `authStringObj.string`, the network request will silently fail, or the Web Worker will hang when attempting to serialize the object into an HTTP Header.
|
||||||
|
|
||||||
|
Always ensure the runtime safely parses masked keys:
|
||||||
|
```javascript
|
||||||
|
const authString = typeof authStringObj === "string" ? authStringObj : (authStringObj.string || "");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Methods Comparison
|
||||||
|
|
||||||
|
| Authentication Method | Security Profile | Setup Complexity | Best For |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **API Key (Restricted)** | Moderate (Long-lived secret, but restricted by referrer) | Low | Permanent embedded recipes, public web hosting, and sharing recipes internally. |
|
||||||
|
| **OAuth Bearer Token** | High (Expires in 1 hr) | Medium (Requires `gcloud` CLI) | Local development, automated E2E testing, and one-off secure executions. |
|
||||||
110
docs/GCloud_APIs_Research_Report.md
Normal file
110
docs/GCloud_APIs_Research_Report.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# Research Report: Expanding CyberChef with Google Cloud APIs for Intelligence Analysis
|
||||||
|
|
||||||
|
## 1. Introduction
|
||||||
|
CyberChef is an invaluable tool for intelligence analysts, incident responders, and forensic investigators. Its "recipe" architecture allows chaining atomic operations to decode, extract, and analyze data. However, native operations are generally limited to deterministic formatting, decoding, and parsing.
|
||||||
|
|
||||||
|
By integrating Google Cloud APIs into CyberChef, we can introduce **advanced AI, machine learning, and external enrichment capabilities** directly into analytic workflows. This report explores how various Google Cloud services can empower analysts, the technical considerations for CyberChef integration (specifically input/output formatting for chaining), and examples of cross-operation workflows.
|
||||||
|
|
||||||
|
## 2. Intelligence Analysis & Data Types
|
||||||
|
Analysts encounter unstructured and semi-structured data from various sources:
|
||||||
|
* **Media:** Images (screenshots, photos), Audio (intercepts, voicemails), Video (CCTV, drone footage).
|
||||||
|
* **Unstructured Text:** Social media posts, dark web chatter, translated documents, threat reports.
|
||||||
|
* **Geospatial Data:** Raw coordinates, place names, IP locations.
|
||||||
|
|
||||||
|
The goal is to transform this raw data into structured intelligence (entities, sentiment, locations, summaries) that can be utilized in downstream CyberChef operations (like extracting IOCs or formatting into CSVs for reporting).
|
||||||
|
|
||||||
|
## 3. Proposed Google Cloud API Operations
|
||||||
|
|
||||||
|
### 3.1. Entity Extraction & Sentiment Analysis
|
||||||
|
**API:** [Cloud Natural Language API](https://cloud.google.com/natural-language)
|
||||||
|
* **Purpose:** Extracting people, organizations, locations, events, and assessing the sentiment of unstructured text.
|
||||||
|
* **Input:** Text (UTF-8).
|
||||||
|
* **Output Considerations:**
|
||||||
|
* *Option 1 (Human Readable):* Formatted text table `[Entity Type] - [Entity Name] (Salience)`.
|
||||||
|
* *Option 2 (Machine Readable/Chaining):* Line-separated list of extracted entities (e.g., just the names) so it can be fed directly into default CyberChef operations like "Defang IP", "Sort", or "Unique".
|
||||||
|
* *Option 3 (JSON):* Full JSON response for advanced users to parse using CyberChef's "JSONPath".
|
||||||
|
|
||||||
|
### 3.2. Image Recognition & OCR
|
||||||
|
**API:** [Cloud Vision API](https://cloud.google.com/vision)
|
||||||
|
* **Purpose:** Identifying objects, landmarks, logos, explicit content, and extracting text (OCR) from images.
|
||||||
|
* **Input:** Image file (Hex/Base64 encoded or raw bytes).
|
||||||
|
* **Output Considerations:**
|
||||||
|
* If the user selects **OCR / Text Extraction**, the output should be purely the extracted text. This allows the output to be seamlessly piped into regex extractors (e.g., "Extract IP addresses").
|
||||||
|
* If the user selects **Label / Object Detection**, the output could be a comma-separated list of tags (e.g., `car, weapon, outdoors`) or a JSON payload detailing bounding boxes. For CyberChef, a flat list of tags is most useful for chaining into text analysis.
|
||||||
|
* *Landmark Detection* could output specific GPS coordinates to be fed into a Maps operation.
|
||||||
|
|
||||||
|
### 3.3. Audio & Video Transcribing
|
||||||
|
**APIs:** [Cloud Speech-to-Text API](https://cloud.google.com/speech-to-text), [Cloud Video Intelligence API](https://cloud.google.com/video-intelligence)
|
||||||
|
* **Purpose:** Converting spoken language in audio/video files into searchable text, and identifying scene changes or objects in video frames.
|
||||||
|
* **Input:** Audio/Video files (bytes). Note: CyberChef runs in-browser, so large files might be memory-constrained. Consider supporting GCS URIs (`gs://...`) as an alternative input for large media.
|
||||||
|
* **Output Considerations:**
|
||||||
|
* Plain text transcription. This effortlessly integrates with CyberChef’s text manipulation operations, Translation operations, and Entity Extraction.
|
||||||
|
* For Video Intelligence, a timeline of detected objects (e.g., `[00:01:23] - Vehicle`).
|
||||||
|
|
||||||
|
### 3.4. Web Search & Enrichment
|
||||||
|
**API:** [Custom Search JSON API](https://developers.google.com/custom-search/v1/overview)
|
||||||
|
* **Purpose:** Querying the web for open-source intelligence (OSINT) related to an extracted indicator (e.g., querying a hash or username).
|
||||||
|
* **Input:** Short text string.
|
||||||
|
* **Output Considerations:**
|
||||||
|
* Extracting purely the URLs found in the search results to feed into a hypothetical "Scrape Webpage" operation.
|
||||||
|
* Returning snippets of text from the search results to be fed into the LLM or Entity Extractor.
|
||||||
|
|
||||||
|
### 3.5. Geocoding & Mapping
|
||||||
|
**API:** [Google Maps Platform (Geocoding API / Maps Static API)](https://developers.google.com/maps)
|
||||||
|
* **Purpose:** Converting addresses into geographic coordinates (Geocoding), or coordinates into geographic contexts (Reverse Geocoding).
|
||||||
|
* **Input:** Address string OR Latitude, Longitude.
|
||||||
|
* **Output Considerations:**
|
||||||
|
* *Geocoding:* Outputs `Lat, Lng`.
|
||||||
|
* *Static Map:* Outputs an Image (PNG/JPG file representation in CyberChef) showing a pin on the map.
|
||||||
|
|
||||||
|
### 3.6. Generic LLM Capabilities (Gemini API)
|
||||||
|
**API:** [Vertex AI Gemini API](https://cloud.google.com/vertex-ai) / [Google AI Studio](https://aistudio.google.com/)
|
||||||
|
* **Purpose:** An all-purpose operation where the analyst provides a System Prompt and User Prompt, and the operation feeds the CyberChef input as context.
|
||||||
|
* **Input:** Any text or supported media (Gemini is multimodal natively).
|
||||||
|
* **Options for the Operation Pane:**
|
||||||
|
* `System Prompt` (e.g., "You are a malware analyst. Extract all indicators of compromise from the following text and return them as a strict CSV.")
|
||||||
|
* `Temperature` / `Model Selection` (e.g., `gemini-1.5-pro`)
|
||||||
|
* **Output Considerations:** Pure text output generated by the LLM. Because LLMs can be instructed to format data (JSON, CSV, lists), this operation is incredibly versatile for chaining to existing CyberChef data parsing tools.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Chaining & Interoperability Considerations
|
||||||
|
|
||||||
|
To make Google Cloud APIs feel like "native" CyberChef ingredients, the boundary between operations must be seamless.
|
||||||
|
|
||||||
|
**The "Format" Dropdown:**
|
||||||
|
Every API operation should ideally have an `Output Format` argument in its UI pane with options like:
|
||||||
|
1. **Raw Text / Flat List:** Best for chaining. (e.g., Vision API outputs `gun, suspect, vehicle` or Speech API purely outputs transcription).
|
||||||
|
2. **Metadata / Human Readable:** A pretty-printed summary.
|
||||||
|
3. **JSON:** Strict API response for advanced jq/JSONPath manipulation further down the recipe.
|
||||||
|
|
||||||
|
**Error Handling:**
|
||||||
|
Cloud APIs can fail (rate limits, bad keys, unreadable media). Operations must fail gracefully within the CyberChef framework, outputting explicit error messages rather than hanging the pipeline, as users might be automatically processing hundreds of files via "Fork".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Potential Workflows (Recipes)
|
||||||
|
|
||||||
|
Here are examples of how analysts could build recipes combining native CyberChef and these proposed GCloud APIs.
|
||||||
|
|
||||||
|
### Workflow A: Media OSINT Exploitation
|
||||||
|
1. **Input:** A foreign-language propaganda video file.
|
||||||
|
2. **GCloud Speech-to-Text:** Extract the audio transcription.
|
||||||
|
3. **GCloud Translate:** Translate the transcription to English.
|
||||||
|
4. **Extract Regular Expression:** Extract potential phone numbers or email addresses mentioned.
|
||||||
|
5. **GCloud Natural Language:** Extract Organizations and Locations mentioned in the translated text.
|
||||||
|
|
||||||
|
### Workflow B: Image-to-Intelligence
|
||||||
|
1. **Input:** A screenshot of a dark web forum post.
|
||||||
|
2. **GCloud Vision API (OCR mode):** Extract the text from the screenshot.
|
||||||
|
3. **GCloud Gemini Prompt:**
|
||||||
|
* *System Prompt:* "Summarize the threat actor's intent in one sentence, then list any mentioned CVEs."
|
||||||
|
* *Input:* Output from Step 2.
|
||||||
|
4. **Output:** A concise, text-based threat intel report ready for a ticket.
|
||||||
|
|
||||||
|
### Workflow C: Location Triangulation
|
||||||
|
1. **Input:** Text document referencing various safehouse addresses.
|
||||||
|
2. **GCloud Natural Language:** Extract entities of type `LOCATION`.
|
||||||
|
3. **Fork:** Split each location into its own execution stream.
|
||||||
|
4. **GCloud Geocoding:** Convert the location names to Lat/Lng coordinates.
|
||||||
|
5. **Output:** A list of coordinates ready to be plotted on an analyst's map.
|
||||||
44
docs/GoogleCloudSetup.md
Normal file
44
docs/GoogleCloudSetup.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Google Cloud Setup Guide for CyberChef
|
||||||
|
|
||||||
|
To use Google Cloud capabilities (like Google Translate) within CyberChef, you need to configure a Google Cloud Project and obtain an authentication string (either an API Key or an OAuth Token).
|
||||||
|
|
||||||
|
## Method 1: API Key (Recommended for simplicity)
|
||||||
|
|
||||||
|
1. **Create a Project:**
|
||||||
|
- Go to the [Google Cloud Console](https://console.cloud.google.com/).
|
||||||
|
- Click on the project dropdown at the top and select **New Project**.
|
||||||
|
- Name your project (e.g., `cyberchefcloud`) and click **Create**.
|
||||||
|
|
||||||
|
2. **Enable the API:**
|
||||||
|
- In the Cloud Console search bar, type **"Cloud Translation API"** and select it.
|
||||||
|
- Click **Enable**.
|
||||||
|
- *(Note: You will need to have billing enabled on your Google Cloud account for the Translation API, even for the free tier).*
|
||||||
|
|
||||||
|
3. **Create the API Key:**
|
||||||
|
- Navigate to **APIs & Services > Credentials** in the left sidebar.
|
||||||
|
- Click **+ CREATE CREDENTIALS** at the top and select **API Key**.
|
||||||
|
- Your API Key will be generated. Copy this key; you will need it for the CyberChef "GCP Auth String" input.
|
||||||
|
|
||||||
|
4. **Secure the API Key (CRITICAL):**
|
||||||
|
- Since CyberChef runs entirely in your browser, your API Key will be visible to anyone you share your CyberChef recipe with or who inspects the network traffic. you **MUST** restrict it.
|
||||||
|
- Click on the newly created API Key to edit its settings.
|
||||||
|
- Under **Application restrictions**, select **Websites**.
|
||||||
|
- Under **Website restrictions**, click **ADD**.
|
||||||
|
- Enter the URLs where your CyberChef instance is hosted (e.g., `https://gchq.github.io/CyberChef/*` or `http://localhost:8080/*` for local testing).
|
||||||
|
- Under **API restrictions**, select **Restrict key**.
|
||||||
|
- Check the box for **Cloud Translation API**.
|
||||||
|
- Click **SAVE**.
|
||||||
|
|
||||||
|
## Method 2: Temporary OAuth Token (Recommended for Security)
|
||||||
|
|
||||||
|
If you have the Google Cloud SDK (`gcloud`) installed locally and you are authorized in your project, you can generate a short-lived token to use instead of an API Key. This is much more secure because the token expires automatically.
|
||||||
|
|
||||||
|
1. Ensure you are logged into your `gcloud` CLI:
|
||||||
|
```bash
|
||||||
|
gcloud auth login
|
||||||
|
```
|
||||||
|
2. Generate an access token:
|
||||||
|
```bash
|
||||||
|
gcloud auth print-access-token
|
||||||
|
```
|
||||||
|
3. Copy the output token and paste it into the CyberChef "GCP Auth String" input, making sure to change the "Auth Type" dropdown to **OAuth Token**.
|
||||||
87
docs/LocalTesting.md
Normal file
87
docs/LocalTesting.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# Local Testing Guide for CyberChef
|
||||||
|
|
||||||
|
If you are developing new Cloud API capabilities (like the Google Translate operation) and you want to test them locally on your machine, follow these steps to spin up the local CyberChef development server.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. **Node.js**: Ensure you have Node.js installed. CyberChef requires Node 16 or later.
|
||||||
|
```bash
|
||||||
|
node -v
|
||||||
|
```
|
||||||
|
2. **NPM**: Ensure you have npm installed.
|
||||||
|
```bash
|
||||||
|
npm -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup and Running
|
||||||
|
|
||||||
|
1. **Install Dependencies:**
|
||||||
|
Open a terminal, navigate to the `CyberChefCloud` directory, and run:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
*This might take a minute as it downloads everything required to build CyberChef.*
|
||||||
|
|
||||||
|
2. **Start the Development Server:**
|
||||||
|
Run the following command to start the local instance:
|
||||||
|
```bash
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
*This will run a Grunt task that compiles the web interface, resolves operations, and provisions a local HTTP server using Webpack Dev Server.*
|
||||||
|
|
||||||
|
3. **Access CyberChef:**
|
||||||
|
By default, the server will host CyberChef on port 8080.
|
||||||
|
- Open your web browser.
|
||||||
|
- Go to `http://localhost:8080`.
|
||||||
|
|
||||||
|
## Testing the Translate Operation
|
||||||
|
|
||||||
|
1. With the local CyberChef instance open, type "Google Translate" into the **Operations** search bar in the top left.
|
||||||
|
2. Drag the `Google Translate` operation into the **Recipe** column.
|
||||||
|
3. In the **Input** column, type some text (e.g., "Hello world").
|
||||||
|
4. In the `Google Translate` operation configuration:
|
||||||
|
- Make sure **Source Language** is correct (e.g., `en`).
|
||||||
|
- Make sure **Target Language** is correct (e.g., `es`).
|
||||||
|
- If using an API key, leave Auth Type as **API Key** and paste your API key into the **GCP Auth String** box.
|
||||||
|
- *(Note: Ensure your API key restrictions at console.cloud.google.com temporarily allow `http://localhost:8080/*`).*
|
||||||
|
5. Check the **Manual Bake** checkbox at the bottom of the recipe column if it isn't checked by default, or just click **Bake!**.
|
||||||
|
6. The translated output should appear in the **Output** column.
|
||||||
|
|
||||||
|
## Advanced Testing (Command Line)
|
||||||
|
|
||||||
|
To ensure the CyberChef engine builds cleanly and passes its internal checks without UI verification, you can run the automated tests:
|
||||||
|
```bash
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
## End-to-End Browser Testing (Nightwatch)
|
||||||
|
|
||||||
|
CyberChef is equipped with Nightwatch for end-to-end testing of features running in a real Chromium browser. This is extremely useful for Cloud Operations that cannot be easily tested within the headless NodeJS environment.
|
||||||
|
|
||||||
|
### Setting up API Keys
|
||||||
|
|
||||||
|
To prevent live API keys from being leaked in CI/CD, the Cloud API browser tests rely on local environment variables:
|
||||||
|
1. Copy the `.env.template` file to `.env`:
|
||||||
|
```bash
|
||||||
|
cp .env.template .env
|
||||||
|
```
|
||||||
|
2. Open `.env` and replace `YOUR_API_KEY_HERE` with your actual Google Cloud API key for the `CYBERCHEF_GCP_TEST_KEY` variable. Ensure the API Key restrictions at console.cloud.google.com are set up to accept requests from `http://localhost:8080/*`.
|
||||||
|
|
||||||
|
### Running Nightwatch Tests
|
||||||
|
|
||||||
|
Once the `.env` file is prepared, you can trigger the entire browser suite:
|
||||||
|
```bash
|
||||||
|
npm run test:browser
|
||||||
|
```
|
||||||
|
|
||||||
|
Or just the Cloud Operations specifically:
|
||||||
|
```bash
|
||||||
|
npx nightwatch tests/browser/03_cloud_ops.js
|
||||||
|
```
|
||||||
|
*Note: Make sure your local CyberChef dev server (`npm run start`) is currently running on `localhost:8080`, as Nightwatch tests require a live application target!*
|
||||||
|
|
||||||
|
#### Troubleshooting (WSL/Linux Environments)
|
||||||
|
If your `nightwatch` tests immediately crash with ChromeDriver status `127` or `1`, your system may be missing Chromium's required graphical libraries. You can view exactly what is missing in `tests/browser/output/*_chromedriver.log`. You will typically need to install these packages on Ubuntu/Debian:
|
||||||
|
```bash
|
||||||
|
sudo apt update && sudo apt install -y libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpangocairo-1.0-0 libasound2
|
||||||
|
```
|
||||||
40
docs/how to test with an LLM.md
Normal file
40
docs/how to test with an LLM.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# How to Test with an LLM
|
||||||
|
|
||||||
|
When developing agentic AI integrations—especially those interacting with Complex UIs and Cloud APIs—automated testing is the only way for the LLM to verify that its own code modifications are successful.
|
||||||
|
|
||||||
|
This document summarizes critical lessons learned during the implementation and verification of the Cloud API (Google Translate) integration using Nightwatch.js as the E2E verification framework. Following these guidelines ensures an LLM can quickly and reliably verify application outputs autonomously.
|
||||||
|
|
||||||
|
## 1. Headless Browser Environments
|
||||||
|
Agents typically operate inside containers, WSL (Windows Subsystem for Linux), or headless virtual machines that lack graphical display servers (X11/Wayland).
|
||||||
|
* **Actionable Advice**: Webdrivers (like ChromeDriver) must be configured explicitly to run without a GUI. Always inject the following Chrome options into the `nightwatch.json` configuration:
|
||||||
|
* `--headless`
|
||||||
|
* `--no-sandbox` (Critical for running as root in Docker/WSL)
|
||||||
|
* `--disable-gpu`
|
||||||
|
* `--disable-dev-shm-usage` (Prevents memory crashes in containerized `/dev/shm`)
|
||||||
|
|
||||||
|
## 2. Secure Credential Management for Agents
|
||||||
|
E2E testing against live Cloud APIs requires real credentials, but these must never be committed to source control or logged in the agent's chat history.
|
||||||
|
* **Actionable Advice**: Use the `dotenv` package.
|
||||||
|
* Provide an `.env.template` so the human operator knows what variables to supply (e.g., `CYBERCHEF_GCP_TEST_KEY=YOUR_API_KEY_HERE`).
|
||||||
|
* Ensure `.env` is strongly `.gitignore`d.
|
||||||
|
* **Graceful Skips**: If the script detects the API key is missing or is exactly the template string, the test block should `return;` or gracefully skip rather than throw a hard failure. This allows public CI pipelines to continue passing.
|
||||||
|
|
||||||
|
## 3. UI Obfuscation vs Data Structures
|
||||||
|
CyberChef provides UI arguments like `toggleString` which are specifically designed to mask sensitive inputs (like API keys turning into `****` on screen). However, changing a UI parameter type fundamentally changes the shape of the data passed to the backend functions.
|
||||||
|
* **Actionable Advice**: An LLM might assume an argument is a plain string. If a UI masking attribute is introduced, the test framework *must* be updated to pass the correct object payload (e.g., passing `{ option: "UTF8", string: "AIzaSy..." }` instead of `"AIzaSy..."`), and the backend worker code must be updated to unpack `authStringObj.string` prior to network fetching.
|
||||||
|
* *Failure to map the data object correctly results in Web Worker crashes or infinite hangs that the test framework cannot concisely diagnose.*
|
||||||
|
|
||||||
|
## 4. Bypassing Legacy Test Utilities
|
||||||
|
Test framework abstractions (such as wrapper polling functions in `browserUtils.js`) can easily break or mask the root cause of an error. In our case, `expectOutput` attempted to check the length of an unresolved promise state, throwing a "Timeout Error" even though the UI had populated the data perfectly.
|
||||||
|
* **Actionable Advice**: When an LLM is getting unhelpful "Timeout" errors despite visually verifying the UI is acting correctly, bypass the utility framework. Write raw Javascript to evaluate state inside the browser context, and return it to the Node runner for standard validation:
|
||||||
|
```javascript
|
||||||
|
browser.execute(function () {
|
||||||
|
return window.app.manager.output.outputEditorView.state.doc.toString();
|
||||||
|
}, [], function ({ value }) {
|
||||||
|
browser.assert.equal(value, "Expected String");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Giving the LLM "Eyes"
|
||||||
|
When an E2E test fails, the LLM only sees a terminal stack trace. It cannot see the state of the UI (e.g., did an error snackbar pop up? Did a preloader hang?).
|
||||||
|
* **Actionable Advice**: Instruct the LLM to aggressively implement screenshot captures during test runs (`browser.saveScreenshot("tests/browser/output/debug.png");`), particularly right before an assertion is about to be checked. The LLM can then visually parse the exact state of the UI grid, inputs, outputs, and overlays, bridging the gap between a generic "TimeoutError" and a specific visual bug.
|
||||||
@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
"src_folders": ["tests/browser"],
|
"src_folders": [
|
||||||
"exclude": ["tests/browser/browserUtils.js"],
|
"tests/browser"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"tests/browser/browserUtils.js"
|
||||||
|
],
|
||||||
"output_folder": "tests/browser/output",
|
"output_folder": "tests/browser/output",
|
||||||
|
|
||||||
"test_settings": {
|
"test_settings": {
|
||||||
|
|
||||||
"default": {
|
"default": {
|
||||||
"launch_url": "http://localhost:8080",
|
"launch_url": "http://localhost:8080",
|
||||||
"webdriver": {
|
"webdriver": {
|
||||||
@ -14,19 +16,23 @@
|
|||||||
"log_path": "tests/browser/output"
|
"log_path": "tests/browser/output"
|
||||||
},
|
},
|
||||||
"desiredCapabilities": {
|
"desiredCapabilities": {
|
||||||
"browserName": "chrome"
|
"browserName": "chrome",
|
||||||
|
"chromeOptions": {
|
||||||
|
"args": [
|
||||||
|
"--no-sandbox",
|
||||||
|
"--headless",
|
||||||
|
"--disable-gpu",
|
||||||
|
"--disable-dev-shm-usage"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"enable_fail_fast": true
|
"enable_fail_fast": true
|
||||||
},
|
},
|
||||||
|
|
||||||
"dev": {
|
"dev": {
|
||||||
"launch_url": "http://localhost:8080"
|
"launch_url": "http://localhost:8080"
|
||||||
},
|
},
|
||||||
|
|
||||||
"prod": {
|
"prod": {
|
||||||
"launch_url": "http://localhost:8000/index.html"
|
"launch_url": "http://localhost:8000/index.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
43
package-lock.json
generated
43
package-lock.json
generated
@ -123,7 +123,7 @@
|
|||||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||||
"babel-plugin-transform-builtin-extend": "1.1.2",
|
"babel-plugin-transform-builtin-extend": "1.1.2",
|
||||||
"base64-loader": "^1.0.0",
|
"base64-loader": "^1.0.0",
|
||||||
"chromedriver": "^130.0.4",
|
"chromedriver": "^145.0.6",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"compression-webpack-plugin": "^11.1.0",
|
"compression-webpack-plugin": "^11.1.0",
|
||||||
@ -131,6 +131,7 @@
|
|||||||
"core-js": "^3.48.0",
|
"core-js": "^3.48.0",
|
||||||
"cspell": "^8.19.4",
|
"cspell": "^8.19.4",
|
||||||
"css-loader": "7.1.4",
|
"css-loader": "7.1.4",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "^9.39.3",
|
||||||
"eslint-plugin-jsdoc": "^48.11.0",
|
"eslint-plugin-jsdoc": "^48.11.0",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
@ -6377,28 +6378,35 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chromedriver": {
|
"node_modules/chromedriver": {
|
||||||
"version": "130.0.4",
|
"version": "145.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-130.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-145.0.6.tgz",
|
||||||
"integrity": "sha512-lpR+PWXszij1k4Ig3t338Zvll9HtCTiwoLM7n4pCCswALHxzmgwaaIFBh3rt9+5wRk9D07oFblrazrBxwaYYAQ==",
|
"integrity": "sha512-qobFdfjk7G7U9GKB6RYGBuqQ8L0QG1M30p90sNIWLKdpeobhsedfBhVxRqT4m/nWAtM0PhNb9GDD9qzDwSSGlA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testim/chrome-version": "^1.1.4",
|
"@testim/chrome-version": "^1.1.4",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.13.5",
|
||||||
"compare-versions": "^6.1.0",
|
"compare-versions": "^6.1.0",
|
||||||
"extract-zip": "^2.0.1",
|
"extract-zip": "^2.0.1",
|
||||||
"proxy-agent": "^6.4.0",
|
"proxy-agent": "^6.4.0",
|
||||||
"proxy-from-env": "^1.1.0",
|
"proxy-from-env": "^2.0.0",
|
||||||
"tcp-port-used": "^1.0.2"
|
"tcp-port-used": "^1.0.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"chromedriver": "bin/chromedriver"
|
"chromedriver": "bin/chromedriver"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chromedriver/node_modules/proxy-from-env": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-h2lD3OfRraP3R51rNFKIE8nX+qoLr1mE74X91YhVxtDbt+OD6ntoNZv56+JgI4RCdtwQ5eexsOk1KdOQDfvPCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ci-info": {
|
"node_modules/ci-info": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz",
|
||||||
@ -8436,16 +8444,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.3.1",
|
"version": "17.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
||||||
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
|
"integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/motdotla/dotenv?sponsor=1"
|
"url": "https://dotenvx.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
@ -13658,6 +13666,19 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nightwatch/node_modules/dotenv": {
|
||||||
|
"version": "16.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
|
||||||
|
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/motdotla/dotenv?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nightwatch/node_modules/glob": {
|
"node_modules/nightwatch/node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
|||||||
@ -55,7 +55,7 @@
|
|||||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||||
"babel-plugin-transform-builtin-extend": "1.1.2",
|
"babel-plugin-transform-builtin-extend": "1.1.2",
|
||||||
"base64-loader": "^1.0.0",
|
"base64-loader": "^1.0.0",
|
||||||
"chromedriver": "^130.0.4",
|
"chromedriver": "^145.0.6",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"compression-webpack-plugin": "^11.1.0",
|
"compression-webpack-plugin": "^11.1.0",
|
||||||
@ -63,6 +63,7 @@
|
|||||||
"core-js": "^3.48.0",
|
"core-js": "^3.48.0",
|
||||||
"cspell": "^8.19.4",
|
"cspell": "^8.19.4",
|
||||||
"css-loader": "7.1.4",
|
"css-loader": "7.1.4",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "^9.39.3",
|
||||||
"eslint-plugin-jsdoc": "^48.11.0",
|
"eslint-plugin-jsdoc": "^48.11.0",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import OperationConfig from "./config/OperationConfig.json" assert {type: "json"};
|
import OperationConfig from "./config/OperationConfig.json" with {type: "json"};
|
||||||
import OperationError from "./errors/OperationError.mjs";
|
import OperationError from "./errors/OperationError.mjs";
|
||||||
import Operation from "./Operation.mjs";
|
import Operation from "./Operation.mjs";
|
||||||
import DishError from "./errors/DishError.mjs";
|
import DishError from "./errors/DishError.mjs";
|
||||||
@ -17,7 +17,7 @@ let modules = null;
|
|||||||
/**
|
/**
|
||||||
* The Recipe controls a list of Operations and the Dish they operate on.
|
* The Recipe controls a list of Operations and the Dish they operate on.
|
||||||
*/
|
*/
|
||||||
class Recipe {
|
class Recipe {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recipe constructor
|
* Recipe constructor
|
||||||
@ -176,7 +176,7 @@ class Recipe {
|
|||||||
* @returns {number}
|
* @returns {number}
|
||||||
* - The final progress through the recipe
|
* - The final progress through the recipe
|
||||||
*/
|
*/
|
||||||
async execute(dish, startFrom=0, forkState={}) {
|
async execute(dish, startFrom = 0, forkState = {}) {
|
||||||
let op, input, output,
|
let op, input, output,
|
||||||
numJumps = 0,
|
numJumps = 0,
|
||||||
numRegisters = forkState.numRegisters || 0;
|
numRegisters = forkState.numRegisters || 0;
|
||||||
@ -204,19 +204,19 @@ class Recipe {
|
|||||||
log.debug(`Executing operation '${op.name}'`);
|
log.debug(`Executing operation '${op.name}'`);
|
||||||
|
|
||||||
if (isWorkerEnvironment()) {
|
if (isWorkerEnvironment()) {
|
||||||
self.sendStatusMessage(`Baking... (${i+1}/${this.opList.length})`);
|
self.sendStatusMessage(`Baking... (${i + 1}/${this.opList.length})`);
|
||||||
self.sendProgressMessage(i + 1, this.opList.length);
|
self.sendProgressMessage(i + 1, this.opList.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (op.flowControl) {
|
if (op.flowControl) {
|
||||||
// Package up the current state
|
// Package up the current state
|
||||||
let state = {
|
let state = {
|
||||||
"progress": i,
|
"progress": i,
|
||||||
"dish": dish,
|
"dish": dish,
|
||||||
"opList": this.opList,
|
"opList": this.opList,
|
||||||
"numJumps": numJumps,
|
"numJumps": numJumps,
|
||||||
"numRegisters": numRegisters,
|
"numRegisters": numRegisters,
|
||||||
"forkOffset": forkState.forkOffset || 0
|
"forkOffset": forkState.forkOffset || 0
|
||||||
};
|
};
|
||||||
|
|
||||||
state = await op.run(state);
|
state = await op.run(state);
|
||||||
@ -339,7 +339,7 @@ class Recipe {
|
|||||||
*/
|
*/
|
||||||
lastOpPresented(progress) {
|
lastOpPresented(progress) {
|
||||||
if (progress < 1) return false;
|
if (progress < 1) return false;
|
||||||
return this.opList[progress-1].presentType !== this.opList[progress-1].outputType;
|
return this.opList[progress - 1].presentType !== this.opList[progress - 1].outputType;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -541,6 +541,12 @@
|
|||||||
"Heatmap chart"
|
"Heatmap chart"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Cloud",
|
||||||
|
"ops": [
|
||||||
|
"Google Translate"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Other",
|
"name": "Other",
|
||||||
"ops": [
|
"ops": [
|
||||||
|
|||||||
60
src/core/lib/GoogleCloud.mjs
Normal file
60
src/core/lib/GoogleCloud.mjs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* @author CyberChefCloud
|
||||||
|
* @copyright Crown Copyright 2026
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common arguments for Google Cloud Platform operations
|
||||||
|
*
|
||||||
|
* Spread these into the `args` array of any new GCP operation.
|
||||||
|
*/
|
||||||
|
export const GCP_AUTH_ARGS = [
|
||||||
|
{
|
||||||
|
"name": "Auth Type",
|
||||||
|
"type": "option",
|
||||||
|
"value": ["API Key", "OAuth Token"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GCP Auth String",
|
||||||
|
"type": "toggleString",
|
||||||
|
"value": "",
|
||||||
|
"toggleValues": ["UTF8", "Latin1", "Base64", "Hex"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Quota Project (ADC only)",
|
||||||
|
"type": "string",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and applies GCP authentication to a URL and Headers object.
|
||||||
|
*
|
||||||
|
* @param {string} url - The base URL of the API endpoint.
|
||||||
|
* @param {Headers} headers - The Headers object for the request.
|
||||||
|
* @param {string} authType - "API Key" or "OAuth Token".
|
||||||
|
* @param {Object|string} authStringObj - The authentication string (can be a toggleString object).
|
||||||
|
* @param {string} quotaProject - Optional quota project for ADC OAuth tokens.
|
||||||
|
* @returns {Object} An object containing the modified { url, headers }
|
||||||
|
*/
|
||||||
|
export function applyGCPAuth(url, headers, authType, authStringObj, quotaProject) {
|
||||||
|
const authString = typeof authStringObj === "string" ? authStringObj : (authStringObj.string || "");
|
||||||
|
|
||||||
|
if (!authString) {
|
||||||
|
throw new OperationError("Error: Please provide a valid GCP Auth String (API Key or OAuth Token).");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authType === "API Key") {
|
||||||
|
url += `${url.includes('?') ? '&' : '?'}key=${encodeURIComponent(authString)}`;
|
||||||
|
} else if (authType === "OAuth Token") {
|
||||||
|
headers.set("Authorization", `Bearer ${authString}`);
|
||||||
|
if (quotaProject) {
|
||||||
|
headers.set("x-goog-user-project", quotaProject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { url, headers };
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import OperationConfig from "../config/OperationConfig.json" assert {type: "json"};
|
import OperationConfig from "../config/OperationConfig.json" with {type: "json"};
|
||||||
import Utils, { isWorkerEnvironment } from "../Utils.mjs";
|
import Utils, { isWorkerEnvironment } from "../Utils.mjs";
|
||||||
import Recipe from "../Recipe.mjs";
|
import Recipe from "../Recipe.mjs";
|
||||||
import Dish from "../Dish.mjs";
|
import Dish from "../Dish.mjs";
|
||||||
|
|||||||
109
src/core/operations/GoogleTranslate.mjs
Normal file
109
src/core/operations/GoogleTranslate.mjs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* @author CyberChefCloud
|
||||||
|
* @copyright Crown Copyright 2016
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Operation from "../Operation.mjs";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import { GCP_AUTH_ARGS, applyGCPAuth } from "../lib/GoogleCloud.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Google Translate operation
|
||||||
|
*/
|
||||||
|
class GoogleTranslate extends Operation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GoogleTranslate constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "Google Translate";
|
||||||
|
this.module = "Cloud";
|
||||||
|
this.description = [
|
||||||
|
"Translates text using the Google Cloud Translation API.",
|
||||||
|
"<br><br>",
|
||||||
|
"Supports providing an API Key or an OAuth Bearer Token. ",
|
||||||
|
"See the setup guide in the documentation for how to secure your Cloud project.",
|
||||||
|
].join("\n");
|
||||||
|
this.infoURL = "https://cloud.google.com/translate/docs/reference/rest/v2/translate";
|
||||||
|
this.inputType = "string";
|
||||||
|
this.outputType = "string";
|
||||||
|
this.manualBake = true;
|
||||||
|
this.args = [
|
||||||
|
{
|
||||||
|
"name": "Source Language (ISO-639-1)",
|
||||||
|
"type": "string",
|
||||||
|
"value": "en"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Target Language (ISO-639-1)",
|
||||||
|
"type": "string",
|
||||||
|
"value": "es"
|
||||||
|
},
|
||||||
|
...GCP_AUTH_ARGS
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
async run(input, args) {
|
||||||
|
const [sourceLanguage, targetLanguage, authType, authStringObj, quotaProject] = args;
|
||||||
|
|
||||||
|
if (input.length === 0) return "";
|
||||||
|
|
||||||
|
let url = "https://translation.googleapis.com/language/translate/v2";
|
||||||
|
let headers = new Headers();
|
||||||
|
headers.set("Content-Type", "application/json; charset=utf-8");
|
||||||
|
|
||||||
|
({ url, headers } = applyGCPAuth(url, headers, authType, authStringObj, quotaProject));
|
||||||
|
|
||||||
|
const body = JSON.stringify({
|
||||||
|
q: input,
|
||||||
|
source: sourceLanguage,
|
||||||
|
target: targetLanguage,
|
||||||
|
format: "text"
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
method: "POST",
|
||||||
|
headers: headers,
|
||||||
|
body: body,
|
||||||
|
mode: "cors",
|
||||||
|
cache: "no-cache",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, config);
|
||||||
|
let responseData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
responseData = await response.json();
|
||||||
|
} catch (err) {
|
||||||
|
throw new OperationError("Error: Failed to parse response from Google Translation API.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const msg = responseData?.error?.message || response.statusText;
|
||||||
|
throw new OperationError(`Google Translation API Error (${response.status}): ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseData && responseData.data && responseData.data.translations && responseData.data.translations.length > 0) {
|
||||||
|
return responseData.data.translations[0].translatedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new OperationError("Error: Unexpected response format from Google Translation API.");
|
||||||
|
} catch (e) {
|
||||||
|
if (e.name === "OperationError") throw e;
|
||||||
|
throw new OperationError(e.message || e.toString() +
|
||||||
|
"\n\nThis error could be caused by a network issue or invalid authentication.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GoogleTranslate;
|
||||||
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
import NodeDish from "./NodeDish.mjs";
|
import NodeDish from "./NodeDish.mjs";
|
||||||
import NodeRecipe from "./NodeRecipe.mjs";
|
import NodeRecipe from "./NodeRecipe.mjs";
|
||||||
import OperationConfig from "../core/config/OperationConfig.json" assert {type: "json"};
|
import OperationConfig from "../core/config/OperationConfig.json" with {type: "json"};
|
||||||
import { sanitise, removeSubheadingsFromArray, sentenceToCamelCase } from "./apiUtils.mjs";
|
import { sanitise, removeSubheadingsFromArray, sentenceToCamelCase } from "./apiUtils.mjs";
|
||||||
import ExcludedOperationError from "../core/errors/ExcludedOperationError.mjs";
|
import ExcludedOperationError from "../core/errors/ExcludedOperationError.mjs";
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import sm from "sitemap";
|
import sm from "sitemap";
|
||||||
import OperationConfig from "../../core/config/OperationConfig.json" assert { type: "json" };
|
import OperationConfig from "../../core/config/OperationConfig.json" with {type: "json"};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates an XML sitemap for all CyberChef operations and a number of recipes.
|
* Generates an XML sitemap for all CyberChef operations and a number of recipes.
|
||||||
|
|||||||
106
tests/browser/03_cloud_ops.js
Normal file
106
tests/browser/03_cloud_ops.js
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* End-to-end tests for Cloud Operations via Nightwatch.
|
||||||
|
*
|
||||||
|
* NOTE: Tests that execute real API calls will be skipped if the required API keys
|
||||||
|
* are not found in the environment variables (e.g. running in a public CI pipeline).
|
||||||
|
*
|
||||||
|
* @author CyberChefCloud
|
||||||
|
* @copyright Crown Copyright 2026
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
const browserUtils = require("./browserUtils.js");
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
|
||||||
|
before: browser => {
|
||||||
|
browser
|
||||||
|
.resizeWindow(1280, 800)
|
||||||
|
.url(browser.launchUrl)
|
||||||
|
.useCss()
|
||||||
|
.waitForElementNotPresent("#preloader", 10000)
|
||||||
|
.click("#auto-bake-label");
|
||||||
|
},
|
||||||
|
|
||||||
|
"Google Translate: Missing Key Validation": function (browser) {
|
||||||
|
browserUtils.loadRecipe(browser, "Google Translate", "Hello World", [
|
||||||
|
"en",
|
||||||
|
"es",
|
||||||
|
"API Key",
|
||||||
|
{ option: "UTF8", string: "" },
|
||||||
|
""
|
||||||
|
]);
|
||||||
|
|
||||||
|
browser.waitForElementNotVisible("#snackbar-container", 6000);
|
||||||
|
browserUtils.bake(browser);
|
||||||
|
browser.pause(2000);
|
||||||
|
browser.execute(function () {
|
||||||
|
return window.app.manager.output.outputEditorView.state.doc.toString();
|
||||||
|
}, [], function ({ value }) {
|
||||||
|
browser.assert.ok(value.includes("Please provide a valid GCP Auth String"));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
"Google Translate: Successful OAuth Token Translation": function (browser) {
|
||||||
|
let testToken = process.env.CYBERCHEF_GCP_TEST_TOKEN;
|
||||||
|
|
||||||
|
if (!testToken || testToken === "YOUR_OAUTH_TOKEN_HERE") {
|
||||||
|
try {
|
||||||
|
testToken = require('child_process').execSync('gcloud auth print-access-token', { stdio: 'pipe', encoding: 'utf-8' }).trim();
|
||||||
|
} catch (e) {
|
||||||
|
console.log("No valid CYBERCHEF_GCP_TEST_TOKEN found and gcloud failed. Skipping live API test.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
browserUtils.loadRecipe(browser, "Google Translate", "Hello", [
|
||||||
|
"en",
|
||||||
|
"es",
|
||||||
|
"OAuth Token",
|
||||||
|
{ option: "UTF8", string: testToken },
|
||||||
|
"cyberchefcloud"
|
||||||
|
]);
|
||||||
|
|
||||||
|
browser.waitForElementNotVisible("#snackbar-container", 6000);
|
||||||
|
browserUtils.bake(browser);
|
||||||
|
browser.pause(2000);
|
||||||
|
browser.saveScreenshot("tests/browser/output/success_oauth_debug.png");
|
||||||
|
browser.execute(function () {
|
||||||
|
return window.app.manager.output.outputEditorView.state.doc.toString();
|
||||||
|
}, [], function ({ value }) {
|
||||||
|
browser.assert.equal(value, "Hola");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
"Google Translate: Successful API Key Translation": function (browser) {
|
||||||
|
const testKey = process.env.CYBERCHEF_GCP_TEST_KEY;
|
||||||
|
|
||||||
|
if (!testKey || testKey === "YOUR_API_KEY_HERE") {
|
||||||
|
console.log("No valid CYBERCHEF_GCP_TEST_KEY found in environment variables. Skipping live API test.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
browserUtils.loadRecipe(browser, "Google Translate", "Hello", [
|
||||||
|
"en",
|
||||||
|
"es",
|
||||||
|
"API Key",
|
||||||
|
{ option: "UTF8", string: testKey },
|
||||||
|
""
|
||||||
|
]);
|
||||||
|
|
||||||
|
browser.waitForElementNotVisible("#snackbar-container", 6000);
|
||||||
|
browserUtils.bake(browser);
|
||||||
|
browser.pause(2000);
|
||||||
|
browser.saveScreenshot("tests/browser/output/success_apikey_debug.png");
|
||||||
|
browser.execute(function () {
|
||||||
|
return window.app.manager.output.outputEditorView.state.doc.toString();
|
||||||
|
}, [], function ({ value }) {
|
||||||
|
browser.assert.equal(value, "Hola");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
after: function (browser) {
|
||||||
|
browser.end();
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -28,7 +28,7 @@ function clear(browser) {
|
|||||||
* @param {boolean} [type=true] - Whether to type the characters in by using sendKeys,
|
* @param {boolean} [type=true] - Whether to type the characters in by using sendKeys,
|
||||||
* or to set the value of the editor directly (useful for special characters)
|
* or to set the value of the editor directly (useful for special characters)
|
||||||
*/
|
*/
|
||||||
function setInput(browser, input, type=true) {
|
function setInput(browser, input, type = true) {
|
||||||
clear(browser);
|
clear(browser);
|
||||||
if (type) {
|
if (type) {
|
||||||
browser
|
browser
|
||||||
@ -105,8 +105,8 @@ function setEOLSeq(browser, io, eol) {
|
|||||||
* @param {Browser} browser - Nightwatch client
|
* @param {Browser} browser - Nightwatch client
|
||||||
*/
|
*/
|
||||||
function copy(browser) {
|
function copy(browser) {
|
||||||
browser.perform(function() {
|
browser.perform(function () {
|
||||||
const actions = this.actions({async: true});
|
const actions = this.actions({ async: true });
|
||||||
|
|
||||||
// Ctrl + Ins used as this works on Windows, Linux and Mac
|
// Ctrl + Ins used as this works on Windows, Linux and Mac
|
||||||
return actions
|
return actions
|
||||||
@ -126,8 +126,8 @@ function copy(browser) {
|
|||||||
function paste(browser, el) {
|
function paste(browser, el) {
|
||||||
browser
|
browser
|
||||||
.click(el)
|
.click(el)
|
||||||
.perform(function() {
|
.perform(function () {
|
||||||
const actions = this.actions({async: true});
|
const actions = this.actions({ async: true });
|
||||||
|
|
||||||
// Shift + Ins used as this works on Windows, Linux and Mac
|
// Shift + Ins used as this works on Windows, Linux and Mac
|
||||||
return actions
|
return actions
|
||||||
@ -150,7 +150,7 @@ function paste(browser, el) {
|
|||||||
function loadRecipe(browser, opName, input, args) {
|
function loadRecipe(browser, opName, input, args) {
|
||||||
let recipeConfig;
|
let recipeConfig;
|
||||||
|
|
||||||
if (typeof(opName) === "string") {
|
if (typeof (opName) === "string") {
|
||||||
recipeConfig = JSON.stringify([{
|
recipeConfig = JSON.stringify([{
|
||||||
"op": opName,
|
"op": opName,
|
||||||
"args": args
|
"args": args
|
||||||
@ -165,7 +165,7 @@ function loadRecipe(browser, opName, input, args) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Invalid operation type. Must be string or array of strings. Received: " + typeof(opName));
|
throw new Error("Invalid operation type. Must be string or array of strings. Received: " + typeof (opName));
|
||||||
}
|
}
|
||||||
|
|
||||||
setInput(browser, input, false);
|
setInput(browser, input, false);
|
||||||
@ -182,19 +182,20 @@ function loadRecipe(browser, opName, input, args) {
|
|||||||
* @param {boolean} [waitNotNull=false] - Wait for the output to not be empty before testing the value
|
* @param {boolean} [waitNotNull=false] - Wait for the output to not be empty before testing the value
|
||||||
* @param {number} [waitWindow=1000] - The number of milliseconds to wait for the output to be correct
|
* @param {number} [waitWindow=1000] - The number of milliseconds to wait for the output to be correct
|
||||||
*/
|
*/
|
||||||
function expectOutput(browser, expected, waitNotNull=false, waitWindow=1000) {
|
function expectOutput(browser, expected, waitNotNull = false, waitWindow = 1000) {
|
||||||
if (waitNotNull && expected !== "") {
|
if (waitNotNull && expected !== "") {
|
||||||
browser.waitUntil(async function() {
|
browser.waitUntil(async function () {
|
||||||
const output = await this.execute(function() {
|
const result = await this.execute(function () {
|
||||||
return window.app.manager.output.outputEditorView.state.doc.toString();
|
return window.app.manager.output.outputEditorView.state.doc.toString();
|
||||||
});
|
});
|
||||||
return output.length;
|
const output = result && result.value !== undefined ? result.value : result;
|
||||||
|
return typeof output === "string" && output.length > 0;
|
||||||
}, waitWindow);
|
}, waitWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.execute(expected => {
|
browser.execute(expected => {
|
||||||
return window.app.manager.output.outputEditorView.state.doc.toString();
|
return window.app.manager.output.outputEditorView.state.doc.toString();
|
||||||
}, [expected], function({value}) {
|
}, [expected], function ({ value }) {
|
||||||
if (expected instanceof RegExp) {
|
if (expected instanceof RegExp) {
|
||||||
browser.expect(value).match(expected);
|
browser.expect(value).match(expected);
|
||||||
} else {
|
} else {
|
||||||
@ -212,7 +213,7 @@ function expectOutput(browser, expected, waitNotNull=false, waitWindow=1000) {
|
|||||||
function expectInput(browser, expected) {
|
function expectInput(browser, expected) {
|
||||||
browser.execute(expected => {
|
browser.execute(expected => {
|
||||||
return window.app.manager.input.inputEditorView.state.doc.toString();
|
return window.app.manager.input.inputEditorView.state.doc.toString();
|
||||||
}, [expected], function({value}) {
|
}, [expected], function ({ value }) {
|
||||||
if (expected instanceof RegExp) {
|
if (expected instanceof RegExp) {
|
||||||
browser.expect(value).match(expected);
|
browser.expect(value).match(expected);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import TestRegister from "../../lib/TestRegister.mjs";
|
import TestRegister from "../../lib/TestRegister.mjs";
|
||||||
import Categories from "../../../src/core/config/Categories.json" assert {type: "json"};
|
import Categories from "../../../src/core/config/Categories.json" with {type: "json"};
|
||||||
import OperationConfig from "../../../src/core/config/OperationConfig.json" assert {type: "json"};
|
import OperationConfig from "../../../src/core/config/OperationConfig.json" with {type: "json"};
|
||||||
import it from "../assertionHandler.mjs";
|
import it from "../assertionHandler.mjs";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
|
|
||||||
|
|||||||
20
tests/operations/tests/GoogleTranslate.mjs
Normal file
20
tests/operations/tests/GoogleTranslate.mjs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import TestRegister from "../../lib/TestRegister.mjs";
|
||||||
|
|
||||||
|
TestRegister.addTests([
|
||||||
|
{
|
||||||
|
name: "Google Translate: Missing Auth String",
|
||||||
|
input: "Hello world",
|
||||||
|
expectedError: "Error: Please provide a valid GCP Auth String (API Key or OAuth Token).",
|
||||||
|
recipeConfig: [
|
||||||
|
{
|
||||||
|
"op": "Google Translate",
|
||||||
|
"args": [
|
||||||
|
"en",
|
||||||
|
"es",
|
||||||
|
"API Key",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
Loading…
x
Reference in New Issue
Block a user