save progress

This commit is contained in:
StellaOps Bot
2026-01-04 19:08:47 +02:00
parent f7d27c6fda
commit 75611a505f
97 changed files with 4531 additions and 293 deletions

View 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"

View File

@@ -0,0 +1,299 @@
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# rotate-secrets-bundle.sh
# Sprint: SPRINT_20260104_005_AIRGAP (Secret Offline Kit Integration)
# Task: OKS-006 - Add bundle rotation/upgrade workflow
# Description: Safely rotate/upgrade secrets rule bundle with backup and rollback
# -----------------------------------------------------------------------------
# Usage: ./rotate-secrets-bundle.sh <new-bundle-path> [install-path]
# Example: ./rotate-secrets-bundle.sh /mnt/offline-kit/rules/secrets/2026.02
set -euo pipefail
# Configuration
NEW_BUNDLE_PATH="${1:?New bundle path required (e.g., /mnt/offline-kit/rules/secrets/2026.02)}"
INSTALL_PATH="${2:-/opt/stellaops/plugins/scanner/analyzers/secrets}"
BACKUP_BASE="${BACKUP_BASE:-/opt/stellaops/backups/secrets-bundles}"
BUNDLE_ID="${BUNDLE_ID:-secrets.ruleset}"
ATTESTOR_MIRROR="${ATTESTOR_MIRROR:-}"
RESTART_WORKERS="${RESTART_WORKERS:-true}"
KUBERNETES_NAMESPACE="${KUBERNETES_NAMESPACE:-stellaops}"
KUBERNETES_DEPLOYMENT="${KUBERNETES_DEPLOYMENT:-scanner-worker}"
MAX_BACKUPS="${MAX_BACKUPS:-5}"
# Script directory for calling install script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Color output helpers
if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'
else
RED=''
GREEN=''
YELLOW=''
BLUE=''
NC=''
fi
log_info() { echo -e "${GREEN}==>${NC} $*"; }
log_warn() { echo -e "${YELLOW}WARN:${NC} $*" >&2; }
log_error() { echo -e "${RED}ERROR:${NC} $*" >&2; }
log_step() { echo -e "${BLUE}--->${NC} $*"; }
# Error handler
cleanup_on_error() {
log_error "Rotation failed! Attempting rollback..."
if [[ -n "${BACKUP_DIR:-}" && -d "${BACKUP_DIR}" ]]; then
perform_rollback "${BACKUP_DIR}"
fi
}
perform_rollback() {
local backup_dir="$1"
log_info "Rolling back to backup: ${backup_dir}"
if [[ ! -d "${backup_dir}" ]]; then
log_error "Backup directory not found: ${backup_dir}"
return 1
fi
# Restore files
cp -a "${backup_dir}"/* "${INSTALL_PATH}/" 2>/dev/null || {
log_error "Failed to restore files from backup"
return 1
}
log_info "Rollback completed"
# Restart workers after rollback
if [[ "${RESTART_WORKERS}" == "true" ]]; then
restart_workers "rollback"
fi
return 0
}
restart_workers() {
local reason="${1:-upgrade}"
log_info "Restarting scanner workers (${reason})..."
# Try Kubernetes first
if command -v kubectl &>/dev/null; then
if kubectl get deployment "${KUBERNETES_DEPLOYMENT}" -n "${KUBERNETES_NAMESPACE}" &>/dev/null; then
log_step "Performing Kubernetes rolling restart..."
kubectl rollout restart deployment/"${KUBERNETES_DEPLOYMENT}" -n "${KUBERNETES_NAMESPACE}"
log_step "Waiting for rollout to complete..."
kubectl rollout status deployment/"${KUBERNETES_DEPLOYMENT}" -n "${KUBERNETES_NAMESPACE}" --timeout=300s || {
log_warn "Rollout status check timed out (workers may still be restarting)"
}
return 0
fi
fi
# Try systemd
if command -v systemctl &>/dev/null; then
if systemctl is-active stellaops-scanner-worker &>/dev/null 2>&1; then
log_step "Restarting systemd service..."
systemctl restart stellaops-scanner-worker
return 0
fi
fi
log_warn "Could not auto-restart workers (no Kubernetes or systemd found)"
log_warn "Please restart scanner workers manually"
}
cleanup_old_backups() {
log_info "Cleaning up old backups (keeping last ${MAX_BACKUPS})..."
if [[ ! -d "${BACKUP_BASE}" ]]; then
return 0
fi
# List backups sorted by name (which includes timestamp)
local backups
backups=$(find "${BACKUP_BASE}" -maxdepth 1 -type d -name "20*" | sort -r)
local count=0
for backup in ${backups}; do
count=$((count + 1))
if [[ ${count} -gt ${MAX_BACKUPS} ]]; then
log_step "Removing old backup: ${backup}"
rm -rf "${backup}"
fi
done
}
# Main rotation logic
main() {
echo ""
log_info "Secrets Bundle Rotation"
echo "========================================"
echo ""
# Validate new bundle
log_info "Step 1/6: Validating new bundle..."
if [[ ! -d "${NEW_BUNDLE_PATH}" ]]; then
log_error "New bundle directory not found: ${NEW_BUNDLE_PATH}"
exit 1
fi
NEW_MANIFEST="${NEW_BUNDLE_PATH}/${BUNDLE_ID}.manifest.json"
if [[ ! -f "${NEW_MANIFEST}" ]]; then
log_error "New bundle manifest not found: ${NEW_MANIFEST}"
exit 1
fi
NEW_VERSION=$(jq -r '.version // "unknown"' "${NEW_MANIFEST}" 2>/dev/null || echo "unknown")
NEW_RULE_COUNT=$(jq -r '.ruleCount // 0' "${NEW_MANIFEST}" 2>/dev/null || echo "0")
log_step "New version: ${NEW_VERSION} (${NEW_RULE_COUNT} rules)"
# Check current installation
log_info "Step 2/6: Checking current installation..."
CURRENT_VERSION="(none)"
CURRENT_RULE_COUNT="0"
if [[ -f "${INSTALL_PATH}/${BUNDLE_ID}.manifest.json" ]]; then
CURRENT_VERSION=$(jq -r '.version // "unknown"' "${INSTALL_PATH}/${BUNDLE_ID}.manifest.json" 2>/dev/null || echo "unknown")
CURRENT_RULE_COUNT=$(jq -r '.ruleCount // 0' "${INSTALL_PATH}/${BUNDLE_ID}.manifest.json" 2>/dev/null || echo "0")
log_step "Current version: ${CURRENT_VERSION} (${CURRENT_RULE_COUNT} rules)"
else
log_step "No current installation found"
fi
# Version comparison
if [[ "${CURRENT_VERSION}" != "(none)" ]]; then
if [[ "${CURRENT_VERSION}" == "${NEW_VERSION}" ]]; then
log_warn "New version (${NEW_VERSION}) is the same as current"
if [[ "${FORCE_ROTATION:-false}" != "true" ]]; then
log_warn "Use FORCE_ROTATION=true to reinstall"
exit 0
fi
elif [[ "${CURRENT_VERSION}" > "${NEW_VERSION}" ]]; then
log_warn "New version (${NEW_VERSION}) is older than current (${CURRENT_VERSION})"
if [[ "${FORCE_ROTATION:-false}" != "true" ]]; then
log_warn "Use FORCE_ROTATION=true to downgrade"
exit 1
fi
fi
fi
echo ""
log_info "Upgrade: ${CURRENT_VERSION} -> ${NEW_VERSION}"
echo ""
# Backup current installation
log_info "Step 3/6: Creating backup..."
BACKUP_DIR="${BACKUP_BASE}/$(date +%Y%m%d_%H%M%S)_${CURRENT_VERSION}"
if [[ -d "${INSTALL_PATH}" && -f "${INSTALL_PATH}/${BUNDLE_ID}.manifest.json" ]]; then
mkdir -p "${BACKUP_DIR}"
cp -a "${INSTALL_PATH}"/* "${BACKUP_DIR}/" 2>/dev/null || {
log_error "Failed to create backup"
exit 1
}
log_step "Backup created: ${BACKUP_DIR}"
# Create backup metadata
cat > "${BACKUP_DIR}/.backup-metadata.json" <<EOF
{
"version": "${CURRENT_VERSION}",
"backupAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"reason": "rotation-to-${NEW_VERSION}",
"hostname": "$(hostname -f 2>/dev/null || hostname)"
}
EOF
else
log_step "No existing installation to backup"
BACKUP_DIR=""
fi
# Set up error handler for rollback
trap cleanup_on_error ERR
# Install new bundle
log_info "Step 4/6: Installing new bundle..."
export FORCE_INSTALL=true
export REQUIRE_SIGNATURE="${REQUIRE_SIGNATURE:-true}"
if [[ -n "${ATTESTOR_MIRROR}" ]]; then
"${SCRIPT_DIR}/install-secrets-bundle.sh" "${NEW_BUNDLE_PATH}" "${INSTALL_PATH}" "${ATTESTOR_MIRROR}"
else
"${SCRIPT_DIR}/install-secrets-bundle.sh" "${NEW_BUNDLE_PATH}" "${INSTALL_PATH}"
fi
# Verify installation
log_info "Step 5/6: Verifying installation..."
INSTALLED_VERSION=$(jq -r '.version' "${INSTALL_PATH}/${BUNDLE_ID}.manifest.json" 2>/dev/null || echo "unknown")
if [[ "${INSTALLED_VERSION}" != "${NEW_VERSION}" ]]; then
log_error "Installation verification failed"
log_error "Expected version: ${NEW_VERSION}"
log_error "Installed version: ${INSTALLED_VERSION}"
exit 1
fi
log_step "Installation verified: ${INSTALLED_VERSION}"
# Remove error trap since installation succeeded
trap - ERR
# Restart workers
log_info "Step 6/6: Restarting workers..."
if [[ "${RESTART_WORKERS}" == "true" ]]; then
restart_workers "upgrade"
else
log_step "Worker restart skipped (RESTART_WORKERS=false)"
fi
# Cleanup old backups
cleanup_old_backups
# Generate rotation report
REPORT_FILE="${INSTALL_PATH}/.rotation-report.json"
cat > "${REPORT_FILE}" <<EOF
{
"previousVersion": "${CURRENT_VERSION}",
"newVersion": "${NEW_VERSION}",
"previousRuleCount": ${CURRENT_RULE_COUNT},
"newRuleCount": ${NEW_RULE_COUNT},
"rotatedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"backupPath": "${BACKUP_DIR:-null}",
"hostname": "$(hostname -f 2>/dev/null || hostname)"
}
EOF
echo ""
echo "========================================"
log_info "Rotation completed successfully!"
echo ""
echo "Summary:"
echo " Previous version: ${CURRENT_VERSION} (${CURRENT_RULE_COUNT} rules)"
echo " New version: ${NEW_VERSION} (${NEW_RULE_COUNT} rules)"
if [[ -n "${BACKUP_DIR}" ]]; then
echo " Backup path: ${BACKUP_DIR}"
fi
echo " Report: ${REPORT_FILE}"
echo ""
echo "To verify the upgrade:"
echo " kubectl logs -l app=scanner-worker --tail=100 | grep SecretsAnalyzerHost"
echo ""
echo "To rollback if needed:"
echo " $0 --rollback ${BACKUP_DIR:-/path/to/backup}"
}
# Handle rollback command
if [[ "${1:-}" == "--rollback" ]]; then
ROLLBACK_BACKUP="${2:?Backup directory required for rollback}"
perform_rollback "${ROLLBACK_BACKUP}"
if [[ "${RESTART_WORKERS}" == "true" ]]; then
restart_workers "rollback"
fi
exit 0
fi
# Run main
main "$@"