save progress
This commit is contained in:
231
devops/offline/scripts/install-secrets-bundle.sh
Normal file
231
devops/offline/scripts/install-secrets-bundle.sh
Normal file
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# install-secrets-bundle.sh
|
||||
# Sprint: SPRINT_20260104_005_AIRGAP (Secret Offline Kit Integration)
|
||||
# Task: OKS-005 - Create bundle installation script
|
||||
# Description: Install signed secrets rule bundle for offline environments
|
||||
# -----------------------------------------------------------------------------
|
||||
# Usage: ./install-secrets-bundle.sh <bundle-path> [install-path] [attestor-mirror]
|
||||
# Example: ./install-secrets-bundle.sh /mnt/offline-kit/rules/secrets/2026.01
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
BUNDLE_PATH="${1:?Bundle path required (e.g., /mnt/offline-kit/rules/secrets/2026.01)}"
|
||||
INSTALL_PATH="${2:-/opt/stellaops/plugins/scanner/analyzers/secrets}"
|
||||
ATTESTOR_MIRROR="${3:-}"
|
||||
BUNDLE_ID="${BUNDLE_ID:-secrets.ruleset}"
|
||||
REQUIRE_SIGNATURE="${REQUIRE_SIGNATURE:-true}"
|
||||
STELLAOPS_USER="${STELLAOPS_USER:-stellaops}"
|
||||
STELLAOPS_GROUP="${STELLAOPS_GROUP:-stellaops}"
|
||||
|
||||
# Color output helpers (disabled if not a terminal)
|
||||
if [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
YELLOW=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
log_info() { echo -e "${GREEN}==>${NC} $*"; }
|
||||
log_warn() { echo -e "${YELLOW}WARN:${NC} $*" >&2; }
|
||||
log_error() { echo -e "${RED}ERROR:${NC} $*" >&2; }
|
||||
|
||||
# Validate bundle path
|
||||
log_info "Validating secrets bundle at ${BUNDLE_PATH}"
|
||||
|
||||
if [[ ! -d "${BUNDLE_PATH}" ]]; then
|
||||
log_error "Bundle directory not found: ${BUNDLE_PATH}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MANIFEST_FILE="${BUNDLE_PATH}/${BUNDLE_ID}.manifest.json"
|
||||
RULES_FILE="${BUNDLE_PATH}/${BUNDLE_ID}.rules.jsonl"
|
||||
SIGNATURE_FILE="${BUNDLE_PATH}/${BUNDLE_ID}.dsse.json"
|
||||
|
||||
if [[ ! -f "${MANIFEST_FILE}" ]]; then
|
||||
log_error "Manifest not found: ${MANIFEST_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${RULES_FILE}" ]]; then
|
||||
log_error "Rules file not found: ${RULES_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract bundle version
|
||||
BUNDLE_VERSION=$(jq -r '.version // "unknown"' "${MANIFEST_FILE}" 2>/dev/null || echo "unknown")
|
||||
RULE_COUNT=$(jq -r '.ruleCount // 0' "${MANIFEST_FILE}" 2>/dev/null || echo "0")
|
||||
SIGNER_KEY_ID=$(jq -r '.signerKeyId // "unknown"' "${MANIFEST_FILE}" 2>/dev/null || echo "unknown")
|
||||
|
||||
log_info "Bundle version: ${BUNDLE_VERSION}"
|
||||
log_info "Rule count: ${RULE_COUNT}"
|
||||
log_info "Signer key ID: ${SIGNER_KEY_ID}"
|
||||
|
||||
# Verify signature if required
|
||||
if [[ "${REQUIRE_SIGNATURE}" == "true" ]]; then
|
||||
log_info "Verifying bundle signature..."
|
||||
|
||||
if [[ ! -f "${SIGNATURE_FILE}" ]]; then
|
||||
log_error "Signature file not found: ${SIGNATURE_FILE}"
|
||||
log_error "Set REQUIRE_SIGNATURE=false to skip signature verification (not recommended)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set attestor mirror URL if provided
|
||||
if [[ -n "${ATTESTOR_MIRROR}" ]]; then
|
||||
export STELLA_ATTESTOR_URL="file://${ATTESTOR_MIRROR}"
|
||||
log_info "Using attestor mirror: ${STELLA_ATTESTOR_URL}"
|
||||
fi
|
||||
|
||||
# Verify using stella CLI if available
|
||||
if command -v stella &>/dev/null; then
|
||||
if ! stella secrets bundle verify --bundle "${BUNDLE_PATH}" --bundle-id "${BUNDLE_ID}"; then
|
||||
log_error "Bundle signature verification failed"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Signature verification passed"
|
||||
else
|
||||
log_warn "stella CLI not found, performing basic signature file check only"
|
||||
|
||||
# Basic check: verify signature file is valid JSON with expected structure
|
||||
if ! jq -e '.payloadType and .payload and .signatures' "${SIGNATURE_FILE}" >/dev/null 2>&1; then
|
||||
log_error "Invalid DSSE envelope structure in ${SIGNATURE_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify payload digest matches
|
||||
EXPECTED_DIGEST=$(jq -r '.payload' "${SIGNATURE_FILE}" | base64 -d | sha256sum | cut -d' ' -f1)
|
||||
ACTUAL_DIGEST=$(sha256sum "${MANIFEST_FILE}" | cut -d' ' -f1)
|
||||
|
||||
if [[ "${EXPECTED_DIGEST}" != "${ACTUAL_DIGEST}" ]]; then
|
||||
log_error "Payload digest mismatch"
|
||||
log_error "Expected: ${EXPECTED_DIGEST}"
|
||||
log_error "Actual: ${ACTUAL_DIGEST}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_warn "Basic signature structure verified (full cryptographic verification requires stella CLI)"
|
||||
fi
|
||||
else
|
||||
log_warn "Signature verification skipped (REQUIRE_SIGNATURE=false)"
|
||||
fi
|
||||
|
||||
# Verify file digests listed in manifest
|
||||
log_info "Verifying file digests..."
|
||||
DIGEST_ERRORS=()
|
||||
|
||||
while IFS= read -r file_entry; do
|
||||
FILE_NAME=$(echo "${file_entry}" | jq -r '.name')
|
||||
EXPECTED_DIGEST=$(echo "${file_entry}" | jq -r '.digest' | sed 's/sha256://')
|
||||
FILE_PATH="${BUNDLE_PATH}/${FILE_NAME}"
|
||||
|
||||
if [[ ! -f "${FILE_PATH}" ]]; then
|
||||
DIGEST_ERRORS+=("File missing: ${FILE_NAME}")
|
||||
continue
|
||||
fi
|
||||
|
||||
ACTUAL_DIGEST=$(sha256sum "${FILE_PATH}" | cut -d' ' -f1)
|
||||
if [[ "${EXPECTED_DIGEST}" != "${ACTUAL_DIGEST}" ]]; then
|
||||
DIGEST_ERRORS+=("Digest mismatch: ${FILE_NAME}")
|
||||
fi
|
||||
done < <(jq -c '.files[]' "${MANIFEST_FILE}" 2>/dev/null)
|
||||
|
||||
if [[ ${#DIGEST_ERRORS[@]} -gt 0 ]]; then
|
||||
log_error "File digest verification failed:"
|
||||
for err in "${DIGEST_ERRORS[@]}"; do
|
||||
log_error " - ${err}"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
log_info "File digests verified"
|
||||
|
||||
# Check existing installation
|
||||
if [[ -d "${INSTALL_PATH}" ]]; then
|
||||
EXISTING_MANIFEST="${INSTALL_PATH}/${BUNDLE_ID}.manifest.json"
|
||||
if [[ -f "${EXISTING_MANIFEST}" ]]; then
|
||||
EXISTING_VERSION=$(jq -r '.version // "unknown"' "${EXISTING_MANIFEST}" 2>/dev/null || echo "unknown")
|
||||
log_info "Existing installation found: version ${EXISTING_VERSION}"
|
||||
|
||||
# Version comparison (CalVer: YYYY.MM)
|
||||
if [[ "${EXISTING_VERSION}" > "${BUNDLE_VERSION}" ]]; then
|
||||
log_warn "Existing version (${EXISTING_VERSION}) is newer than bundle (${BUNDLE_VERSION})"
|
||||
log_warn "Use FORCE_INSTALL=true to override"
|
||||
if [[ "${FORCE_INSTALL:-false}" != "true" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create installation directory
|
||||
log_info "Creating installation directory: ${INSTALL_PATH}"
|
||||
mkdir -p "${INSTALL_PATH}"
|
||||
|
||||
# Install bundle files
|
||||
log_info "Installing bundle files..."
|
||||
for file in "${BUNDLE_PATH}"/${BUNDLE_ID}.*; do
|
||||
if [[ -f "${file}" ]]; then
|
||||
FILE_NAME=$(basename "${file}")
|
||||
echo " ${FILE_NAME}"
|
||||
cp -f "${file}" "${INSTALL_PATH}/"
|
||||
fi
|
||||
done
|
||||
|
||||
# Set permissions
|
||||
log_info "Setting file permissions..."
|
||||
chmod 640 "${INSTALL_PATH}"/${BUNDLE_ID}.* 2>/dev/null || true
|
||||
|
||||
# Set ownership if running as root
|
||||
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
|
||||
if id "${STELLAOPS_USER}" &>/dev/null; then
|
||||
chown "${STELLAOPS_USER}:${STELLAOPS_GROUP}" "${INSTALL_PATH}"/${BUNDLE_ID}.* 2>/dev/null || true
|
||||
log_info "Set ownership to ${STELLAOPS_USER}:${STELLAOPS_GROUP}"
|
||||
else
|
||||
log_warn "User ${STELLAOPS_USER} does not exist, skipping ownership change"
|
||||
fi
|
||||
else
|
||||
log_info "Not running as root, skipping ownership change"
|
||||
fi
|
||||
|
||||
# Create installation receipt
|
||||
RECEIPT_FILE="${INSTALL_PATH}/.install-receipt.json"
|
||||
cat > "${RECEIPT_FILE}" <<EOF
|
||||
{
|
||||
"bundleId": "${BUNDLE_ID}",
|
||||
"version": "${BUNDLE_VERSION}",
|
||||
"ruleCount": ${RULE_COUNT},
|
||||
"signerKeyId": "${SIGNER_KEY_ID}",
|
||||
"installedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"installedFrom": "${BUNDLE_PATH}",
|
||||
"installedBy": "${USER:-unknown}",
|
||||
"hostname": "$(hostname -f 2>/dev/null || hostname)"
|
||||
}
|
||||
EOF
|
||||
|
||||
# Verify installation
|
||||
INSTALLED_VERSION=$(jq -r '.version' "${INSTALL_PATH}/${BUNDLE_ID}.manifest.json" 2>/dev/null || echo "unknown")
|
||||
log_info "Successfully installed secrets bundle version ${INSTALLED_VERSION}"
|
||||
|
||||
echo ""
|
||||
echo "Installation summary:"
|
||||
echo " Bundle ID: ${BUNDLE_ID}"
|
||||
echo " Version: ${INSTALLED_VERSION}"
|
||||
echo " Rule count: ${RULE_COUNT}"
|
||||
echo " Install path: ${INSTALL_PATH}"
|
||||
echo " Receipt: ${RECEIPT_FILE}"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Restart Scanner Worker to load the new bundle:"
|
||||
echo " systemctl restart stellaops-scanner-worker"
|
||||
echo ""
|
||||
echo " Or with Kubernetes:"
|
||||
echo " kubectl rollout restart deployment/scanner-worker -n stellaops"
|
||||
echo ""
|
||||
echo " 2. Verify bundle is loaded:"
|
||||
echo " kubectl logs -l app=scanner-worker --tail=100 | grep SecretsAnalyzerHost"
|
||||
Reference in New Issue
Block a user