├── README.md ├── lib ├── bm.sh └── cloud.sh └── zap /README.md: -------------------------------------------------------------------------------- 1 | # flashbox :zap: :package: 2 | 3 | flashbox is an opinionated Confidential VM (CVM) base image built for podman pod payloads. With a focus on security balanced against TCB size, designed to give developers the simplest path to TDX VMs. 4 | 5 | ## Why flashbox? 6 | 7 | One command, `./zap` - and you've got yourself a TDX box. 8 | 9 | ⚠️ **IMPORTANT**: This is an early development release and is not production-ready software. Use with caution. 10 | 11 | ## Quick Start 12 | 13 | 1. Download the latest VM image from the releases page 14 | 15 | 2. Deploy the flashbox VM: 16 | ```bash 17 | # Local deployment (non-TDX) 18 | ./zap --mode normal 19 | 20 | # Local deployment (TDX) 21 | ./zap --mode tdx 22 | 23 | # Azure deployment 24 | ./zap azure myvm eastus 25 | 26 | # GCP deployment 27 | ./zap gcp myvm us-east4 28 | ``` 29 | 30 | ### Known Issues 31 | 32 | - Azure deployments may encounter an issue with the `--security-type` parameter. See [Azure CLI Issue #29207](https://github.com/Azure/azure-cli/issues/29207#issuecomment-2479343290) for the workaround. 33 | 34 | ### Considerations 35 | 36 | ⚠️ **WARNING**: Debug releases come with SSH enabled and a root user without password. Always use the `--ssh-source-ip` option to restrict SSH access in cloud deployments. 37 | ⚠️ **IMPORTANT**: If you want to run TDX VMs on bare metal you need to first setup your host environment properly. For this, follow the instructions in the [canonical/tdx](https://github.com/canonical/tdx) repo. 38 | 39 | 3. Provision and start your containers: 40 | ```bash 41 | # Upload pod configuration and environment variables 42 | curl -X POST -F "pod.yaml=@pod.yaml" -F "env=@env" http://flashbox:24070/upload 43 | 44 | # Start the containers 45 | curl -X POST http://flashbox:24070/start 46 | ``` 47 | 48 | ## Pod Configuration 49 | 50 | ### Docker Compose Migration 51 | 52 | If you're coming from Docker Compose, you can convert your existing configurations: 53 | ```bash 54 | podman-compose generate-k8s docker-compose.yml > pod.yaml 55 | ``` 56 | 57 | See the [official documentation on differences between Docker Compose and Podman](https://docs.podman.io/en/latest/markdown/podman-compose.1.html) for migration details. 58 | 59 | ### Example Configuration 60 | 61 | Here's a basic example of a pod configuration: 62 | 63 | ```yaml 64 | apiVersion: v1 65 | kind: Pod 66 | metadata: 67 | name: my-app 68 | labels: 69 | app: my-app 70 | spec: 71 | containers: 72 | - name: web-container 73 | image: nginx:latest 74 | env: 75 | - name: DATABASE_URL 76 | value: "${DATABASE_URL}" 77 | ports: 78 | - containerPort: 80 79 | hostPort: 8080 80 | ``` 81 | ### Non-Attestable Variable Configuration 82 | 83 | flashbox allows you to provision secrets and configuration variables that should remain outside the attestation flow. This is done through a separate `env` file that is processed independently of the pod configuration. 84 | 85 | 1. Create an `env` file with your variables: 86 | ```bash 87 | DATABASE_URL=postgresql://localhost:5432/mydb 88 | API_KEY=your-secret-key 89 | ``` 90 | 91 | 2. Reference these variables in your pod configuration using the `${VARIABLE}` syntax: 92 | ```yaml 93 | env: 94 | - name: DATABASE_URL 95 | value: "${DATABASE_URL}" 96 | - name: API_KEY 97 | value: "${API_KEY}" 98 | ``` 99 | 100 | Variables in the env file will be substituted into the pod configuration at runtime, keeping them separate from the attestation process. This is useful for both secrets and configuration that may vary between deployments. 101 | -------------------------------------------------------------------------------- /lib/bm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script can run both standard QEMU VMs and TDX-enabled VMs 4 | 5 | usage() { 6 | echo "Usage: $0 [options]" 7 | echo "Options:" 8 | echo " --mode VM mode (default: normal)" 9 | echo " --image PATH Path to VM image (optional, will download flashbox.raw if not provided)" 10 | echo " --ram SIZE RAM size in GB (default: 32)" 11 | echo " --cpus NUMBER Number of CPUs (default: 16)" 12 | echo " --ssh-port PORT SSH port forwarding (default: 10022)" 13 | echo " --ports PORTS Additional ports to open, comma-separated" 14 | echo " --name STRING Process name (default: qemu-vm)" 15 | echo " --log PATH Log file path (default: /tmp/qemu-guest.log)" 16 | echo " --ovmf PATH Path to OVMF firmware (default: /usr/share/ovmf/OVMF.fd)" 17 | echo " --help Show this help message" 18 | exit 1 19 | } 20 | 21 | cleanup() { 22 | rm -f /tmp/qemu-guest*.log &> /dev/null 23 | rm -f /tmp/qemu-*-monitor.sock &> /dev/null 24 | 25 | PID_QEMU=$(cat /tmp/qemu-pid.pid 2> /dev/null) 26 | [ ! -z "$PID_QEMU" ] && echo "Cleanup, kill VM with PID: ${PID_QEMU}" && kill -TERM ${PID_QEMU} &> /dev/null 27 | sleep 3 28 | } 29 | 30 | download_flashbox() { 31 | if [ -f "flashbox.raw" ]; then 32 | echo "Using existing flashbox.raw" 33 | else 34 | echo "Downloading flashbox.raw..." 35 | DOWNLOAD_URL=$(curl -s https://api.github.com/repos/flashbots/flashbox/releases/latest | grep "browser_download_url.*flashbox\.raw" | cut -d '"' -f 4) 36 | if [ -z "$DOWNLOAD_URL" ]; then 37 | echo "Error: Could not find download URL for flashbox.raw" 38 | exit 1 39 | fi 40 | wget "$DOWNLOAD_URL" || { 41 | echo "Error: Failed to download flashbox.raw" 42 | exit 1 43 | } 44 | fi 45 | echo "flashbox.raw is ready" 46 | VM_IMG="flashbox.raw" 47 | } 48 | 49 | # Default values 50 | MODE="normal" 51 | RAM_SIZE="32" 52 | CPUS="16" 53 | SSH_PORT="10022" 54 | ADDITIONAL_PORTS="" 55 | PROCESS_NAME="qemu-vm" 56 | LOGFILE="/tmp/qemu-guest.log" 57 | OVMF_PATH="/usr/share/ovmf/OVMF.fd" 58 | VM_IMG="" 59 | 60 | # Parse command line arguments 61 | while [[ $# -gt 0 ]]; do 62 | case $1 in 63 | --mode) 64 | MODE="$2" 65 | shift 2 66 | ;; 67 | --image) 68 | VM_IMG="$2" 69 | shift 2 70 | ;; 71 | --ram) 72 | RAM_SIZE="$2" 73 | shift 2 74 | ;; 75 | --cpus) 76 | CPUS="$2" 77 | shift 2 78 | ;; 79 | --ssh-port) 80 | SSH_PORT="$2" 81 | shift 2 82 | ;; 83 | --ports) 84 | ADDITIONAL_PORTS="$2" 85 | shift 2 86 | ;; 87 | --name) 88 | PROCESS_NAME="$2" 89 | shift 2 90 | ;; 91 | --log) 92 | LOGFILE="$2" 93 | shift 2 94 | ;; 95 | --ovmf) 96 | OVMF_PATH="$2" 97 | shift 2 98 | ;; 99 | --help) 100 | usage 101 | ;; 102 | *) 103 | echo "Unknown option: $1" 104 | usage 105 | ;; 106 | esac 107 | done 108 | 109 | # If no image path provided, download flashbox.raw 110 | if [ -z "$VM_IMG" ]; then 111 | download_flashbox 112 | fi 113 | 114 | # Verify mode 115 | if [ "$MODE" != "normal" ] && [ "$MODE" != "tdx" ]; then 116 | echo "Error: Invalid mode. Must be 'normal' or 'tdx'" 117 | usage 118 | fi 119 | 120 | # Check KVM group membership 121 | if ! groups | grep -qw "kvm"; then 122 | echo "Please add user $USER to kvm group to run this script (usermod -aG kvm $USER and then log in again)." 123 | exit 1 124 | fi 125 | 126 | # Clean up any existing instances 127 | cleanup 128 | if [ "$1" = "clean" ]; then 129 | exit 0 130 | fi 131 | 132 | # Prepare port forwarding string 133 | PORT_FORWARDS="-device virtio-net-pci,netdev=nic0 -netdev user,id=nic0,hostfwd=tcp::${SSH_PORT}-:22" 134 | 135 | # Add default flashbox ports 136 | PORT_FORWARDS="${PORT_FORWARDS},hostfwd=tcp::24070-:24070,hostfwd=tcp::24071-:24071" 137 | 138 | # Add additional ports if specified 139 | if [ ! -z "$ADDITIONAL_PORTS" ]; then 140 | IFS=',' read -ra PORTS <<< "$ADDITIONAL_PORTS" 141 | for port in "${PORTS[@]}"; do 142 | PORT_FORWARDS="${PORT_FORWARDS},hostfwd=tcp::${port}-:${port}" 143 | done 144 | fi 145 | 146 | # Base QEMU command 147 | QEMU_CMD="qemu-system-x86_64 -D $LOGFILE \ 148 | -accel kvm \ 149 | -m ${RAM_SIZE}G -smp $CPUS \ 150 | -name ${PROCESS_NAME},process=${PROCESS_NAME},debug-threads=on \ 151 | -cpu host \ 152 | -nographic \ 153 | -nodefaults \ 154 | -daemonize \ 155 | ${PORT_FORWARDS} \ 156 | -drive file=${VM_IMG},if=none,id=virtio-disk0 \ 157 | -device virtio-blk-pci,drive=virtio-disk0 \ 158 | -bios ${OVMF_PATH} \ 159 | -chardev null,id=char0 \ 160 | -serial chardev:char0 \ 161 | -pidfile /tmp/qemu-pid.pid" 162 | 163 | # Add TDX-specific parameters if mode is tdx 164 | if [ "$MODE" = "tdx" ]; then 165 | QEMU_CMD="$QEMU_CMD \ 166 | -object '{\"qom-type\":\"tdx-guest\",\"id\":\"tdx\",\"quote-generation-socket\":{\"type\": \"vsock\", \"cid\":\"2\",\"port\":\"4050\"}}' \ 167 | -machine q35,kernel_irqchip=split,confidential-guest-support=tdx,hpet=off \ 168 | -device vhost-vsock-pci,guest-cid=4" 169 | else 170 | QEMU_CMD="$QEMU_CMD \ 171 | -machine q35" 172 | fi 173 | 174 | # Execute QEMU command 175 | eval $QEMU_CMD 176 | 177 | ret=$? 178 | if [ $ret -ne 0 ]; then 179 | echo "Error: Failed to create VM. Please check logfile \"$LOGFILE\" for more information." 180 | exit $ret 181 | fi 182 | 183 | PID_QEMU=$(cat /tmp/qemu-pid.pid) 184 | 185 | echo "VM started in $MODE mode with PID: ${PID_QEMU}" 186 | echo "To login via SSH: ssh -p $SSH_PORT root@localhost" 187 | -------------------------------------------------------------------------------- /lib/cloud.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | usage() { 5 | echo "Usage: $0 [command] [options] [region] [image-path]" 6 | echo "" 7 | echo "Commands:" 8 | echo " deploy Deploy a new VM (default if no command specified)" 9 | echo " cleanup Remove all resources for the given name" 10 | echo "" 11 | echo "Cloud Platforms:" 12 | echo " azure Deploy to Azure" 13 | echo " gcp Deploy to Google Cloud Platform" 14 | echo "" 15 | echo "Arguments:" 16 | echo " name Resource name/prefix for the deployment" 17 | echo " region Cloud region to deploy in (default: westeurope for Azure, us-east4 for GCP)" 18 | echo " image-path Path to VM image (optional, will download appropriate image if not provided)" 19 | echo "" 20 | echo "Options:" 21 | echo " --machine-type TYPE VM size (default: Standard_EC4eds_v5 for Azure, c3-standard-4 for GCP)" 22 | echo " --ports PORTS Additional ports to open, comma-separated (24070,24071 always open)" 23 | echo " --ssh-source-ip IP Restrict SSH access to this IP address" 24 | exit 1 25 | } 26 | 27 | download_flashbox() { 28 | local cloud=$1 29 | local image_name 30 | local expected_file 31 | 32 | if [[ "$cloud" == "azure" ]]; then 33 | image_name="flashbox.azure.vhd" 34 | expected_file="$image_name" 35 | else 36 | image_name="flashbox.raw.tar.gz" 37 | expected_file="$image_name" 38 | fi 39 | 40 | if [ -f "$expected_file" ]; then 41 | echo "Using existing $expected_file" 42 | else 43 | echo "Downloading $image_name..." 44 | local DOWNLOAD_URL=$(curl -s https://api.github.com/repos/flashbots/flashbox/releases/latest | grep "browser_download_url.*${image_name}" | cut -d '"' -f 4) 45 | if [ -z "$DOWNLOAD_URL" ]; then 46 | echo "Error: Could not find download URL for $image_name" 47 | exit 1 48 | fi 49 | wget "$DOWNLOAD_URL" || { 50 | echo "Error: Failed to download $image_name" 51 | exit 1 52 | } 53 | fi 54 | echo "$expected_file is ready" 55 | echo "$expected_file" 56 | } 57 | 58 | check_dependencies() { 59 | local cloud=$1 60 | if [[ "$cloud" == "azure" ]]; then 61 | command -v az >/dev/null 2>&1 || { echo "Error: 'az' required"; exit 1; } 62 | command -v azcopy >/dev/null 2>&1 || { echo "Error: 'azcopy' required"; exit 1; } 63 | command -v jq >/dev/null 2>&1 || { echo "Error: 'jq' required"; exit 1; } 64 | elif [[ "$cloud" == "gcp" ]]; then 65 | command -v gcloud >/dev/null 2>&1 || { echo "Error: 'gcloud' required"; exit 1; } 66 | fi 67 | } 68 | 69 | cleanup_azure() { 70 | local name=$1 71 | echo "Cleaning up Azure resources for $name..." 72 | az group delete --name "$name" --yes 73 | } 74 | 75 | cleanup_gcp() { 76 | local name=$1 77 | local label="flashbox-deployment=$name" 78 | 79 | echo "Cleaning up GCP resources for $name..." 80 | 81 | # Delete VM instance 82 | gcloud compute instances delete "$name" --quiet || true 83 | 84 | # Delete image 85 | gcloud compute images delete "$name" --quiet || true 86 | 87 | # Delete firewall rules 88 | gcloud compute firewall-rules list --filter="labels.$label" --format="get(name)" | \ 89 | while read -r rule; do 90 | gcloud compute firewall-rules delete "$rule" --quiet || true 91 | done 92 | 93 | # Delete network 94 | gcloud compute networks delete "$name" --quiet || true 95 | 96 | # Delete storage bucket 97 | gcloud storage rm -r "gs://${name}" || true 98 | } 99 | 100 | create_azure_deployment() { 101 | local name=$1 102 | local region=$2 103 | local image_path=$3 104 | local machine_type=${4:-"Standard_EC4eds_v5"} 105 | local ssh_source_ip=$5 106 | local additional_ports=$6 107 | 108 | # Create resource group 109 | echo "Creating resource group..." 110 | az group create --name "$name" --location "$region" 111 | 112 | # Create and upload disk 113 | echo "Creating and uploading disk..." 114 | local disk_size=$(wc -c < "$image_path") 115 | az disk create -n "$name" -g "$name" -l "$region" \ 116 | --os-type Linux \ 117 | --upload-type Upload \ 118 | --upload-size-bytes "$disk_size" \ 119 | --sku standard_lrs \ 120 | --security-type ConfidentialVM_NonPersistedTPM \ 121 | --hyper-v-generation V2 122 | 123 | # Upload VHD 124 | local sas_json=$(az disk grant-access -n "$name" -g "$name" --access-level Write --duration-in-seconds 86400) 125 | local sas_uri=$(echo "$sas_json" | jq -r '.accessSas') 126 | azcopy copy "$image_path" "$sas_uri" --blob-type PageBlob 127 | az disk revoke-access -n "$name" -g "$name" 128 | 129 | # Create NSG with base rules 130 | echo "Creating network security group..." 131 | az network nsg create --name "$name" --resource-group "$name" --location "$region" 132 | 133 | # Add SSH rule with optional IP restriction 134 | local ssh_source="${ssh_source_ip:-*}" 135 | az network nsg rule create --nsg-name "$name" --resource-group "$name" \ 136 | --name AllowSSH --priority 100 \ 137 | --source-address-prefixes "$ssh_source" \ 138 | --destination-port-ranges 22 --access Allow --protocol Tcp 139 | 140 | # Add default ports 141 | az network nsg rule create --nsg-name "$name" --resource-group "$name" \ 142 | --name "FlashboxAPI" --priority 200 \ 143 | --destination-port-ranges 24070-24071 --access Allow --protocol Tcp 144 | 145 | # Add additional port rules if specified 146 | if [[ -n "$additional_ports" ]]; then 147 | IFS=',' read -ra PORTS <<< "$additional_ports" 148 | local priority=300 149 | for port in "${PORTS[@]}"; do 150 | az network nsg rule create --nsg-name "$name" --resource-group "$name" \ 151 | --name "Port_${port}" --priority $priority \ 152 | --destination-port-ranges "$port" --access Allow --protocol Tcp 153 | ((priority+=1)) 154 | done 155 | fi 156 | 157 | # Create VM 158 | echo "Creating VM..." 159 | az vm create --name "$name" \ 160 | --resource-group "$name" \ 161 | --size "$machine_type" \ 162 | --attach-os-disk "$name" \ 163 | --security-type ConfidentialVM \ 164 | --enable-vtpm true \ 165 | --enable-secure-boot false \ 166 | --os-disk-security-encryption-type NonPersistedTPM \ 167 | --os-type Linux \ 168 | --nsg "$name" 169 | } 170 | 171 | create_gcp_deployment() { 172 | local name=$1 173 | local region=$2 174 | local image_path=$3 175 | local machine_type=${4:-"c3-standard-4"} 176 | local ssh_source_ip=$5 177 | local additional_ports=$6 178 | 179 | local zone="${region}-b" # Assuming zone b 180 | local deployment_label="flashbox-deployment=$name" 181 | 182 | # Create network if it doesn't exist 183 | echo "Creating network..." 184 | gcloud compute networks create "$name" \ 185 | --subnet-mode=auto \ 186 | --labels="$deployment_label" || true 187 | 188 | # Create firewall rules 189 | echo "Creating firewall rules..." 190 | 191 | # SSH rule with optional IP restriction 192 | local ssh_source="${ssh_source_ip:-0.0.0.0/0}" 193 | gcloud compute firewall-rules create "${name}-ssh" \ 194 | --network="$name" \ 195 | --allow=tcp:22 \ 196 | --source-ranges="$ssh_source" \ 197 | --labels="$deployment_label" 198 | 199 | # Default ports 200 | gcloud compute firewall-rules create "${name}-flashbox" \ 201 | --network="$name" \ 202 | --allow=tcp:24070-24071 \ 203 | --labels="$deployment_label" 204 | 205 | # Additional ports if specified 206 | if [[ -n "$additional_ports" ]]; then 207 | local ports_list="tcp:${additional_ports//,/,tcp:}" 208 | gcloud compute firewall-rules create "${name}-ports" \ 209 | --network="$name" \ 210 | --allow="$ports_list" \ 211 | --labels="$deployment_label" 212 | fi 213 | 214 | # Upload and create image 215 | echo "Creating storage bucket and uploading image..." 216 | gcloud storage buckets create "gs://${name}" --labels="$deployment_label" 217 | gcloud storage cp "$image_path" "gs://${name}/image.tar.gz" 218 | 219 | echo "Creating VM image..." 220 | gcloud compute images create "$name" \ 221 | --source-uri="gs://${name}/image.tar.gz" \ 222 | --guest-os-features=UEFI_COMPATIBLE,VIRTIO_SCSI_MULTIQUEUE,GVNIC,TDX_CAPABLE \ 223 | --labels="$deployment_label" 224 | 225 | echo "Creating VM..." 226 | gcloud compute instances create "$name" \ 227 | --zone="$zone" \ 228 | --machine-type="$machine_type" \ 229 | --network="$name" \ 230 | --image="$name" \ 231 | --confidential-compute-type=TDX \ 232 | --maintenance-policy=TERMINATE \ 233 | --labels="$deployment_label" 234 | } 235 | 236 | # Parse command line arguments 237 | COMMAND="deploy" 238 | CLOUD="" 239 | NAME="" 240 | REGION="" 241 | IMAGE_PATH="" 242 | MACHINE_TYPE="" 243 | SSH_SOURCE_IP="" 244 | ADDITIONAL_PORTS="" 245 | 246 | # Check if first arg is a command 247 | case $1 in 248 | deploy|cleanup) 249 | COMMAND="$1" 250 | shift 251 | ;; 252 | esac 253 | 254 | while [[ $# -gt 0 ]]; do 255 | case $1 in 256 | --machine-type) 257 | MACHINE_TYPE="$2" 258 | shift 2 259 | ;; 260 | --ports) 261 | ADDITIONAL_PORTS="$2" 262 | shift 2 263 | ;; 264 | --ssh-source-ip) 265 | SSH_SOURCE_IP="$2" 266 | shift 2 267 | ;; 268 | --help) 269 | usage 270 | ;; 271 | *) 272 | if [[ -z "$CLOUD" ]]; then 273 | CLOUD="$1" 274 | elif [[ -z "$NAME" ]]; then 275 | NAME="$1" 276 | elif [[ -z "$REGION" ]]; then 277 | REGION="$1" 278 | elif [[ -z "$IMAGE_PATH" ]]; then 279 | IMAGE_PATH="$1" 280 | else 281 | echo "Unknown argument: $1" 282 | usage 283 | fi 284 | shift 285 | ;; 286 | esac 287 | done 288 | 289 | # Validate required arguments 290 | if [[ -z "$CLOUD" ]] || [[ -z "$NAME" ]]; then 291 | echo "Error: Missing required arguments" 292 | usage 293 | fi 294 | 295 | if [[ "$CLOUD" != "azure" ]] && [[ "$CLOUD" != "gcp" ]]; then 296 | echo "Error: Invalid cloud platform. Must be 'azure' or 'gcp'" 297 | usage 298 | fi 299 | 300 | # Set default region if not specified for deploy command 301 | if [[ "$COMMAND" == "deploy" && -z "$REGION" ]]; then 302 | REGION=$([ "$CLOUD" == "azure" ] && echo "westeurope" || echo "us-east4") 303 | fi 304 | 305 | # Execute command 306 | case $COMMAND in 307 | deploy) 308 | check_dependencies "$CLOUD" 309 | if [[ -z "$IMAGE_PATH" ]]; then 310 | IMAGE_PATH=$(download_flashbox "$CLOUD") 311 | fi 312 | if [[ "$CLOUD" == "azure" ]]; then 313 | create_azure_deployment "$NAME" "$REGION" "$IMAGE_PATH" "$MACHINE_TYPE" "$SSH_SOURCE_IP" "$ADDITIONAL_PORTS" 314 | else 315 | create_gcp_deployment "$NAME" "$REGION" "$IMAGE_PATH" "$MACHINE_TYPE" "$SSH_SOURCE_IP" "$ADDITIONAL_PORTS" 316 | fi 317 | ;; 318 | cleanup) 319 | if [[ "$CLOUD" == "azure" ]]; then 320 | cleanup_azure "$NAME" 321 | else 322 | cleanup_gcp "$NAME" 323 | fi 324 | ;; 325 | esac 326 | 327 | echo "${COMMAND} complete!" 328 | -------------------------------------------------------------------------------- /zap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 4 | 5 | # Early detection if this is a cloud operation 6 | is_cloud_operation() { 7 | [[ "$1" == "azure" ]] || [[ "$1" == "gcp" ]] || \ 8 | [[ "$2" == "azure" ]] || [[ "$2" == "gcp" ]] 9 | } 10 | 11 | if is_cloud_operation "$1" "$2"; then 12 | exec "${SCRIPT_DIR}/lib/cloud.sh" "$@" 13 | else 14 | exec "${SCRIPT_DIR}/lib/bm.sh" "$@" 15 | fi 16 | --------------------------------------------------------------------------------