From a8cd1c324c0541b0d26542168aeced085ec13201 Mon Sep 17 00:00:00 2001 From: doc Date: Mon, 30 Jun 2025 20:14:17 +0000 Subject: initial failzero commit --- LICENSE | 9 ++ README.md | 86 +++++++++++++++++++ check-hardened.sh | 37 ++++++++ check_rdns_retry.sh | 27 ++++++ functions/destroy_vps_by_label.sh | 28 +++++++ functions/disable_backups_by_label.sh | 23 +++++ functions/disable_ip.sh | 18 ++++ functions/enable_backups_by_label.sh | 23 +++++ functions/list_all_vps.sh | 9 ++ functions/provision.sh | 135 ++++++++++++++++++++++++++++++ functions/reboot_vps.sh | 7 ++ functions/resize_vps.sh | 27 ++++++ functions/safe_create_dataset.sh | 12 +++ functions/status_vps.sh | 8 ++ functions/usage.sh | 22 +++++ functions/verify_ptr.sh | 29 +++++++ genesis_squeaky.sh | 44 ++++++++++ genesisctl.sh | 104 +++++++++++++++++++++++ vps/.env | 5 ++ vps/check-hardened.sh | 37 ++++++++ vps/check_rdns_retry.sh | 27 ++++++ vps/functions/destroy_vps_by_label.sh | 28 +++++++ vps/functions/disable_backups_by_label.sh | 23 +++++ vps/functions/disable_ip.sh | 18 ++++ vps/functions/enable_backups_by_label.sh | 23 +++++ vps/functions/list_all_vps.sh | 9 ++ vps/functions/provision.sh | 135 ++++++++++++++++++++++++++++++ vps/functions/reboot_vps.sh | 7 ++ vps/functions/resize_vps.sh | 27 ++++++ vps/functions/safe_create_dataset.sh | 12 +++ vps/functions/status_vps.sh | 8 ++ vps/functions/usage.sh | 22 +++++ vps/functions/verify_ptr.sh | 29 +++++++ vps/genesis_squeaky.sh | 44 ++++++++++ vps/genesisctl.sh | 104 +++++++++++++++++++++++ 35 files changed, 1206 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100755 check-hardened.sh create mode 100755 check_rdns_retry.sh create mode 100755 functions/destroy_vps_by_label.sh create mode 100755 functions/disable_backups_by_label.sh create mode 100644 functions/disable_ip.sh create mode 100755 functions/enable_backups_by_label.sh create mode 100755 functions/list_all_vps.sh create mode 100755 functions/provision.sh create mode 100755 functions/reboot_vps.sh create mode 100755 functions/resize_vps.sh create mode 100755 functions/safe_create_dataset.sh create mode 100755 functions/status_vps.sh create mode 100755 functions/usage.sh create mode 100755 functions/verify_ptr.sh create mode 100755 genesis_squeaky.sh create mode 100755 genesisctl.sh create mode 100644 vps/.env create mode 100755 vps/check-hardened.sh create mode 100755 vps/check_rdns_retry.sh create mode 100755 vps/functions/destroy_vps_by_label.sh create mode 100755 vps/functions/disable_backups_by_label.sh create mode 100644 vps/functions/disable_ip.sh create mode 100755 vps/functions/enable_backups_by_label.sh create mode 100755 vps/functions/list_all_vps.sh create mode 100755 vps/functions/provision.sh create mode 100755 vps/functions/reboot_vps.sh create mode 100755 vps/functions/resize_vps.sh create mode 100755 vps/functions/safe_create_dataset.sh create mode 100755 vps/functions/status_vps.sh create mode 100755 vps/functions/usage.sh create mode 100755 vps/functions/verify_ptr.sh create mode 100755 vps/genesis_squeaky.sh create mode 100755 vps/genesisctl.sh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6f2010e --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2025 doctator + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c0b26e --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# FailZero + +**FailZero** is a disaster resilience and ransomware recovery platform built by Genesis Hosting Technologies. + +We’re not here to sell you backups — we’re here to help you survive failure. +Whether it’s an encrypted drive, a botched update, or a full datacenter fire, FailZero ensures you have a path forward. + +> _"Hope is not a strategy. Recovery is."_ — FailZero Field Ops Manual + +--- + +## 🛠 What It Does + +FailZero is designed for reactive and proactive DR (disaster recovery) workflows: + +- 🔁 **Live Backup & Mirroring** — Sync media, configs, and databases in near real-time +- 🚨 **Ransomware Containment** — Immutable storage options + airgapped fallback +- 💾 **Versioned Archives** — Roll back to any known good state +- 🧩 **Hybrid Architecture** — Works with ZFS, MinIO, rclone, Samba, PostgreSQL, and more +- 📦 **Disaster Drill Framework** — Simulate failure, validate recovery + +--- + +## 📦 Core Components + +- `genesisctl` – unified CLI to manage snapshots, syncs, and alerts +- `GenesisTimer` – hourly integrity & latency monitor with alerts +- `dr_spl_overlay.sh` – SPL server resilience and fallback handler +- `krang_ops_bot` – Telegram integration for all alerts and status reports + +--- + +## 🔐 Supported Services + +FailZero DR packages are available for: + +- 🎙️ **StationPlaylist** (SPL) +- 📻 **AzuraCast** +- 🐘 **Mastodon** +- 📁 Any self-hosted Linux application with a filesystem and heartbeat + +--- + +## 🚨 Example: Ransomware Workflow + +1. Primary storage compromised (e.g. `/mnt/ransom`) +2. FailZero detects integrity drift or timeout +3. Auto-switch to mirror: `/mnt/failzero` +4. Alerts fired via Telegram, Web, or CLI +5. Optional: Trigger airgapped restore from cold site + +--- + +## 📄 Documentation + +The [FailZero Field Operations Manual](docs/FailZero_FieldOps.pdf) includes: + +- DR runbooks +- Mock exercise scripts +- Deployment guides +- Legal disclaimers + +--- + +## 💬 Contact + +Want to onboard or simulate a failure? + +- Telegram: [@krang_ops_bot](https://t.me/krang_ops_bot) +- Email: `failzero@genesishostingtechnologies.com` +- Status: [status.genesis-radio.net](https://status.genesis-radio.net) + +--- + +## 💥 Warning + +> **FailZero is a disaster recovery tool, not a magic wand.** +> Regular testing, airgaps, and ops discipline are required to avoid data loss. + +--- + +## License + +FailZero is © 2025 Genesis Hosting Technologies. +Usage and distribution subject to terms in `LICENSE`. + diff --git a/check-hardened.sh b/check-hardened.sh new file mode 100755 index 0000000..cdaeef8 --- /dev/null +++ b/check-hardened.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# check-hardened.sh - Scan all known Genesis VPSes for hardening status +# Requirements: ssh access to all VPSes by label or IP + +LOG_BASE="/home/doc/vpslogs" +MARKER_FILE="/var/log/genesis-hardened.ok" + +if [ ! -d "$LOG_BASE" ]; then + echo "❌ Log directory $LOG_BASE does not exist. Are you running this on Krang?" + exit 1 +fi + +cd "$LOG_BASE" || exit 1 + +echo "🔍 Scanning for hardened Genesis VPSes..." +echo + +for LOG in *.log; do + VPS_LABEL="${LOG%.log}" + LAST_KNOWN_IP=$(grep -Eo '\([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\)' "$LOG" | tail -1 | tr -d '()') + + if [ -z "$LAST_KNOWN_IP" ]; then + echo "⚠️ $VPS_LABEL: No IP found in log. Skipping." + continue + fi + + echo -n "🔧 $VPS_LABEL ($LAST_KNOWN_IP): " + + ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no root@"$LAST_KNOWN_IP" "test -f $MARKER_FILE" >/dev/null 2>&1 + + if [ $? -eq 0 ]; then + echo "✅ Hardened" + else + echo "❌ Not marked as hardened" + fi + +done diff --git a/check_rdns_retry.sh b/check_rdns_retry.sh new file mode 100755 index 0000000..b11208b --- /dev/null +++ b/check_rdns_retry.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e +[ -f ".env" ] && source .env +LOGFILE="/home/doc/vpslogs/pending_rdns.log" +TMPFILE="/tmp/rdns_retry.log" + +touch "$TMPFILE" + +while IFS="|" read -r LINODE_ID IP LABEL; do + CURRENT_RDNS=$(dig -x "$IP" +short) + EXPECTED_RDNS="$LABEL.failzero.net." + + if [[ "$CURRENT_RDNS" == "$EXPECTED_RDNS" ]]; then + echo "✅ $IP already has correct rDNS ($CURRENT_RDNS)" + else + echo "⏳ rDNS not set correctly for $LABEL ($IP). Retrying..." + RESPONSE=$(curl -s -X PUT "https://api.linode.com/v4/linode/instances/$LINODE_ID/ips/$IP" \ + -H "Authorization: Bearer $LINODE_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"rdns": "'"$LABEL.failzero.net"'"}') + echo "🔁 Retry result for $IP: $RESPONSE" + fi + + echo "$LINODE_ID|$IP|$LABEL" >> "$TMPFILE" +done < "$LOGFILE" + +mv "$TMPFILE" "$LOGFILE" diff --git a/functions/destroy_vps_by_label.sh b/functions/destroy_vps_by_label.sh new file mode 100755 index 0000000..09d807e --- /dev/null +++ b/functions/destroy_vps_by_label.sh @@ -0,0 +1,28 @@ +destroy_vps_by_label() { + LABEL="$1" + echo "Looking for VPS with label '$LABEL'..." + LINODE_ID=$(curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" \ + https://api.linode.com/v4/linode/instances | \ + jq -r --arg LABEL "$LABEL" '.data[] | select(.label == $LABEL) | .id') + + if [ -z "$LINODE_ID" ]; then + echo "Error: No Linode found with label '$LABEL'" + exit 1 + fi + + read -rp "Are you sure you want to destroy VPS '$LABEL' (ID: $LINODE_ID)? [y/N] " confirm + if [[ "$confirm" =~ ^[Yy]$ ]]; then + echo "Destroying Linode with ID $LINODE_ID (label: $LABEL)..." + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ + https://api.linode.com/v4/linode/instances/$LINODE_ID \ + -H "Authorization: Bearer $LINODE_API_TOKEN") + + if [[ "$HTTP_STATUS" == "204" ]]; then + echo "✅ Linode $LABEL (ID $LINODE_ID) has been destroyed." + else + echo "❌ Failed to destroy VPS. HTTP status: $HTTP_STATUS" + fi + else + echo "Cancelled. VPS '$LABEL' not destroyed." + fi +} diff --git a/functions/disable_backups_by_label.sh b/functions/disable_backups_by_label.sh new file mode 100755 index 0000000..417bdb8 --- /dev/null +++ b/functions/disable_backups_by_label.sh @@ -0,0 +1,23 @@ +disable_backups_by_label() { + LABEL="$1" + LINODE_ID=$(curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" \ + https://api.linode.com/v4/linode/instances | \ + jq -r --arg LABEL "$LABEL" '.data[] | select(.label == $LABEL) | .id') + + if [ -z "$LINODE_ID" ]; then + echo "❌ No Linode found with label '$LABEL'" + exit 1 + fi + + echo "Disabling backups for Linode '$LABEL' (ID: $LINODE_ID)..." + + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + https://api.linode.com/v4/linode/instances/$LINODE_ID/backups/disable \ + -H "Authorization: Bearer $LINODE_API_TOKEN") + + if [[ "$HTTP_STATUS" == "200" ]]; then + echo "✅ Backups disabled for Linode $LABEL." + else + echo "❌ Failed to disable backups (HTTP $HTTP_STATUS)" + fi +} diff --git a/functions/disable_ip.sh b/functions/disable_ip.sh new file mode 100644 index 0000000..0021b74 --- /dev/null +++ b/functions/disable_ip.sh @@ -0,0 +1,18 @@ +disable_ip() { + local ip="$1" + + if [[ -z "$ip" ]]; then + echo "[!] No IP specified." + exit 1 + fi + + echo "[*] Disabling access to VPS with IP: $ip" + + # Block all traffic to/from that IP via iptables + iptables -A INPUT -s "$ip" -j DROP + iptables -A OUTPUT -d "$ip" -j DROP + + echo "$ip - disabled on $(date)" >> /var/log/genesis-disabled.log + + echo "[✓] $ip has been blocked and logged." +} diff --git a/functions/enable_backups_by_label.sh b/functions/enable_backups_by_label.sh new file mode 100755 index 0000000..08fb31d --- /dev/null +++ b/functions/enable_backups_by_label.sh @@ -0,0 +1,23 @@ +enable_backups_by_label() { + LABEL="$1" + LINODE_ID=$(curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" \ + https://api.linode.com/v4/linode/instances | \ + jq -r --arg LABEL "$LABEL" '.data[] | select(.label == $LABEL) | .id') + + if [ -z "$LINODE_ID" ]; then + echo "❌ No Linode found with label '$LABEL'" + exit 1 + fi + + echo "Enabling backups for Linode '$LABEL' (ID: $LINODE_ID)..." + + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + https://api.linode.com/v4/linode/instances/$LINODE_ID/backups/enable \ + -H "Authorization: Bearer $LINODE_API_TOKEN") + + if [[ "$HTTP_STATUS" == "200" ]]; then + echo "✅ Backups enabled for Linode $LABEL." + else + echo "❌ Failed to enable backups (HTTP $HTTP_STATUS)" + fi +} diff --git a/functions/list_all_vps.sh b/functions/list_all_vps.sh new file mode 100755 index 0000000..8ce99eb --- /dev/null +++ b/functions/list_all_vps.sh @@ -0,0 +1,9 @@ +list_all_vps() { + curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" \ + https://api.linode.com/v4/linode/instances | \ + jq -r ' + .data[] | [.label, .id, .region, .type, .ipv4[0], .status] | + @tsv' | column -t -s $'\t' | \ + awk 'BEGIN { print "LABEL ID REGION TYPE IP STATUS" } + { printf "%-11s %-10s %-10s %-16s %-15s %s\n", $1, $2, $3, $4, $5, $6 }' +} diff --git a/functions/provision.sh b/functions/provision.sh new file mode 100755 index 0000000..f6e9d39 --- /dev/null +++ b/functions/provision.sh @@ -0,0 +1,135 @@ +provision_vps() { + LABEL="$1" + REGION="$2" + TYPE="$3" + IMAGE="$4" + ROOT_PASS="${5:-$(openssl rand -base64 16)}" + + if [[ "$LINODE_API_TOKEN" == "REPLACE_WITH_YOUR_LINODE_API_TOKEN" ]]; then + echo "❌ Error: You must set your LINODE_API_TOKEN at the top of this script." + exit 1 + fi + + CLOUD_INIT=$(cat </dev/null || true + systemctl disable linode-cloudinit 2>/dev/null || true + touch /etc/cloud/cloud-init.disabled + rm -rf /etc/cloud /var/lib/cloud /var/log/cloud-init.log + + rm -f /etc/motd /etc/update-motd.d/linode + rm -rf /usr/share/linode* + rm -f /etc/apt/sources.list.d/linode.list + apt remove --purge -y linode-cli linode-config 2>/dev/null || true + + echo "[genesisctl] Attempting to log to Krang via webhook..." >> /var/log/genesis-harden.log + curl -s -X POST -H "Content-Type: application/json" \ + -d "{\"host\": \"$GEN_HOSTNAME\", \"ip\": \"$IP_ADDR\", \"timestamp\": \"$(date)\"}" \ + http://krang.core.sshjunkie.com:8080/genesislog >> /var/log/genesis-harden.log 2>&1 || echo "[genesisctl] Krang webhook logging failed" >> /var/log/genesis-harden.log + + touch /var/log/genesis-hardened.ok + +runcmd: + - [ bash, /usr/local/bin/genesis_squeaky.sh ] +EOF +) + + USER_DATA=$(echo "$CLOUD_INIT" | base64 -w 0) + + echo "Provisioning VPS '$LABEL' in $REGION with type $TYPE and image $IMAGE..." + TMP_FILE=$(mktemp) + JSON_PAYLOAD=$(cat <> /home/doc/vpslogs/pending_rdns.log +} diff --git a/functions/reboot_vps.sh b/functions/reboot_vps.sh new file mode 100755 index 0000000..2741b9c --- /dev/null +++ b/functions/reboot_vps.sh @@ -0,0 +1,7 @@ +reboot_vps() { + LINODE_ID="$1" + echo "Rebooting Linode VPS ID $LINODE_ID..." + + curl -s -X POST https://api.linode.com/v4/linode/instances/$LINODE_ID/reboot \ + -H "Authorization: Bearer $LINODE_API_TOKEN" | jq +} diff --git a/functions/resize_vps.sh b/functions/resize_vps.sh new file mode 100755 index 0000000..c06ea91 --- /dev/null +++ b/functions/resize_vps.sh @@ -0,0 +1,27 @@ +resize_vps() { + LABEL="$1" + NEW_TYPE="$2" + + LINODE_ID=$(curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" \ + https://api.linode.com/v4/linode/instances | \ + jq -r --arg LABEL "$LABEL" '.data[] | select(.label == $LABEL) | .id') + + if [ -z "$LINODE_ID" ]; then + echo "❌ No Linode found with label '$LABEL'" + exit 1 + fi + + echo "Resizing Linode '$LABEL' to type '$NEW_TYPE'..." + + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $LINODE_API_TOKEN" \ + -d '{"type": "'"$NEW_TYPE"'"}' \ + https://api.linode.com/v4/linode/instances/$LINODE_ID/resize) + + if [[ "$HTTP_STATUS" == "200" ]]; then + echo "✅ Linode $LABEL resized to $NEW_TYPE." + else + echo "❌ Failed to resize VPS. HTTP status: $HTTP_STATUS" + fi +} diff --git a/functions/safe_create_dataset.sh b/functions/safe_create_dataset.sh new file mode 100755 index 0000000..1960e55 --- /dev/null +++ b/functions/safe_create_dataset.sh @@ -0,0 +1,12 @@ +safe_create_dataset() { + FULLPATH="$1" + + # Remove any trailing slash + FULLPATH="${FULLPATH%/}" + + POOL="${FULLPATH%%/*}" + DATASET="${FULLPATH#*/}" + + echo "🛰 Connecting to Shredder to safely create '${POOL}/${DATASET}'..." + ssh shredder "/usr/local/bin/genesis-safe-zfs.sh $POOL $DATASET" +} diff --git a/functions/status_vps.sh b/functions/status_vps.sh new file mode 100755 index 0000000..91996e9 --- /dev/null +++ b/functions/status_vps.sh @@ -0,0 +1,8 @@ +status_vps() { + LABEL="$1" + curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" \ + https://api.linode.com/v4/linode/instances | \ + jq -r --arg LABEL "$LABEL" ' + .data[] | select(.label == $LABEL) | + "Label: \(.label)\nID: \(.id)\nRegion: \(.region)\nType: \(.type)\nStatus: \(.status)\nIP: \(.ipv4[0])\nCreated: \(.created)"' +} diff --git a/functions/usage.sh b/functions/usage.sh new file mode 100755 index 0000000..25861b8 --- /dev/null +++ b/functions/usage.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +function usage() { + echo "Usage: genesisctl [command]" + echo "Commands:" + echo " watch-abuse Start abuse monitoring via IPTables" +} + +function watch_abuse() { + echo "[*] Launching abuse watch via screen..." + screen -dmS abusewatch /usr/local/bin/genesisctl-watch-abuse.sh + echo "[✓] Abuse watch running in detached screen session 'abusewatch'" +} + +case "$1" in + watch-abuse) + watch_abuse + ;; + *) + usage + ;; +esac diff --git a/functions/verify_ptr.sh b/functions/verify_ptr.sh new file mode 100755 index 0000000..8ce2f6c --- /dev/null +++ b/functions/verify_ptr.sh @@ -0,0 +1,29 @@ +verify_ptr() { + LABEL="$1" + IP=$(curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" https://api.linode.com/v4/linode/instances \ + | jq -r --arg LABEL "$LABEL" '.data[] | select(.label == $LABEL) | .ipv4[0]') + LINODE_ID=$(curl -s -H "Authorization: Bearer $LINODE_API_TOKEN" https://api.linode.com/v4/linode/instances \ + | jq -r --arg LABEL "$LABEL" '.data[] | select(.label == $LABEL) | .id') + + if [[ -z "$IP" || -z "$LINODE_ID" ]]; then + echo "❌ Could not retrieve IP or Linode ID for label '$LABEL'" + return 1 + fi + + echo "Re-attempting rDNS update for $LABEL ($IP)..." + PTR_NAME="${LABEL}.doinkle.pro" + RDNS_PAYLOAD=$(cat </dev/null || true +systemctl disable linode-cloudinit 2>/dev/null || true +touch /etc/cloud/cloud-init.disabled +rm -rf /etc/cloud /var/lib/cloud /var/log/cloud-init.log +echo "[+] Cloud-init neutered." + +# === STEP 4: Scrub Linode Stuff === +echo "[*] Scrubbing Linode fingerprints..." +rm -f /etc/motd /etc/update-motd.d/linode +rm -rf /usr/share/linode* +rm -f /etc/apt/sources.list.d/linode.list +apt remove --purge -y linode-cli linode-config 2>/dev/null || true +yum remove -y linode-cli linode-config 2>/dev/null || true +echo "[+] Linode packages and branding removed." + +# === STEP 5: Optional Telegram Notice === +# Uncomment if you want to alert yourself when a VPS is hardened +# curl -s -X POST "$TG_API_URL" -d chat_id="$TG_CHAT_ID" -d text="Genesis VPS hardened: $GEN_HOSTNAME is stealth-ready." > /dev/null + +# === STEP 6: Final Touch === +echo "[✅] Genesis VPS hardened. You are now off-the-grid and good to go." diff --git a/genesisctl.sh b/genesisctl.sh new file mode 100755 index 0000000..21fdf7d --- /dev/null +++ b/genesisctl.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# genesisctl - Genesis VPS Provisioning and Reboot CLI +# Usage: +# genesisctl provision