src/opt/modsecurity/configure-rules.sh
#!/bin/sh

# This script is compatible with busybox. ShellCheck requires the shebang
# to be `/bin/busybox sh`, but non-busybox shells will obviously choke on
# that. So we use `/bin/sh` to launch the "default" shell.

set -e

printf "# # #\nRunning CRS rule configuration\n- - -\n"

DIRECTORY="$(cd "$(dirname "$0")" && pwd)"

# Check if crs-setup.conf is overriden
if [ -n "${MANUAL_MODE}" ]; then
  echo "Using manual config mode"
  # Don't use exit on a sourced script
  return
fi

# Default config file path
setup_conf_path="/etc/modsecurity.d/owasp-crs/crs-setup.conf"

# Accept optional config file
if [ -n "${CONFIG_FILE}" ]; then
  setup_conf_path="${CONFIG_FILE}"
fi

# Detect CRS version based on the config file content
detect_crs_version() {
  if grep -q "tx\.blocking_paranoia_level" "${setup_conf_path}"; then
    echo "v4"
  elif grep -q "tx\.paranoia_level" "${setup_conf_path}"; then
    echo "v3"
  else
    echo "Unknown CRS version"
    exit 10
  fi
}

# Get the CRS version
CRS_VERSION="$(detect_crs_version)"
echo "Detected CRS config file version: ${CRS_VERSION}"

set_value() {
  rule="${1}"
  var_name="${2}"
  tx_var_name="${3}"
  var_value="${4}"

  echo "Configuring ${rule} for ${var_name} with ${tx_var_name}=${var_value}"

  # For each rule, we do one pass to uncomment the rule (up to first blank line after the rule),
  # then a second pass to set the variable. We do two separate passes since the rule might
  # already be uncommented (by default in the file or due to having been uncommented in a previous step).
  if grep -Eq "#.*id:${rule}" "${setup_conf_path}"; then
    # Commented, uncomment now
    ed -s "${setup_conf_path}" <<EOF 2 > /dev/null
/id:${rule}/
-
.,/^#\?$/ s/#//
wq
EOF
  fi

  # Uncommented, set var
  # Some rules set multiple vars, so the variable name will be terminated
  # by either `,`, `'`, or `"`, depending on whether it's the last line of the rule
  # and whether the expression is enclosed in single quotes.
  # Use `#` as pattern delimiter, as `/` is part of some variable values.
  # Try to find and update the variable (with or without quotes)
  ed -s "${setup_conf_path}" <<EOF 2 > /dev/null
/id:${rule}/
/setvar:'\?tx\.${tx_var_name}=/
s#=[^,'"]\+#=${var_value}#
wq
EOF
}

should_set() {
  test -n "${1}"
}

can_set() {
  rule="${1}"
  tx_var_name="${2}"

  if ! grep -q "id:${rule}" "${setup_conf_path}"; then
    return 1
  elif ! grep -Eq "setvar:'?tx\.${tx_var_name}" "${setup_conf_path}"; then
    return 1
  fi
  return 0
}

get_legacy() {
  echo "${1}" | awk -F'\|' '{print $1}'
}

get_var_name() {
  echo "${1}" | awk -F'\|' '{print $2}'
}

get_var_value() {
  # Get the variable name, produce "${<var name>}" and use eval to expand
  eval "echo $(echo "${1}" | awk -F'\|' '{print "${"$2"}"}' || true)"
}

get_rule() {
  echo "${1}" | awk -F'\|' '{print $3}'
}

get_tx_var_name() {
  echo "${1}" | awk -F'\|' '{print $4}'
}

while read -r line; do
  if [ -z "${line}" ] || echo "${line}" | grep -Eq "^#"; then
    continue
  fi

  legacy="$(get_legacy "${line}")"
  var_name="$(get_var_name "${line}")"
  var_value="$(get_var_value "${line}")"
  rule="$(get_rule "${line}")"
  tx_var_name="$(get_tx_var_name "${line}")"
  if should_set "${var_value}" "${tx_var_name}"; then
    if ! can_set "${rule}" "${tx_var_name}"; then
      if [ "${legacy}" = "true" ]; then
        echo "Legacy variable ${var_name} (${rule}) set but nothing found to substitute. Skipping"
        continue
      fi
      echo "Failed to find rule ${rule} to set ${tx_var_name}=${var_value} for ${var_name} in ${setup_conf_path}. Aborting"
      exit 1
    fi

    set_value "${rule}" "${var_name}" "${tx_var_name}" "${var_value}"
  fi
done < "${DIRECTORY}/configure-rules.${CRS_VERSION}.conf"

# Add SecDefaultActions
var="${MODSEC_DEFAULT_PHASE1_ACTION}"
if should_set "${var}"; then
  if ! grep -Eq "^SecDefaultAction.*phase:1" "${setup_conf_path}"; then
    echo "Failed to find definition of SecDefaultAction for phase 1 in ${setup_conf_path}. Aborting"
    exit 2
  fi
  ed -s "${setup_conf_path}" <<EOF 2 > /dev/null
/^SecDefaultAction.*phase:1/
s/".*"/"${var}"/
wq
EOF
fi
var="${MODSEC_DEFAULT_PHASE2_ACTION}"
if should_set "${var}"; then
  if ! grep -Eq "^SecDefaultAction.*phase:2" "${setup_conf_path}"; then
    echo "Failed to find definition of SecDefaultAction for phase 2 in ${setup_conf_path}. Aborting"
    exit 3
  fi
  ed -s "${setup_conf_path}" <<EOF 2 > /dev/null
/^SecDefaultAction.*phase:2/
s/".*"/"${var}"/
wq
EOF
fi

# Substitute MODSEC_TAG (part of the default phase actions above)
var="${MODSEC_TAG}"
if should_set "${var}"; then
  if ! grep -q "MODSEC_TAG" "${setup_conf_path}"; then
    echo "Failed to find definition of MODSEC_TAG in ${setup_conf_path}. Skipping"

  else
    sed -z -E -i "s/\\$\{MODSEC_TAG\}/${var}/g" "${setup_conf_path}"
  fi
fi


# Add marker rule for CRS test setup
# Add it only once
if [ -n "${CRS_ENABLE_TEST_MARKER}" ] && [ "${CRS_ENABLE_TEST_MARKER}" -eq 1 ] && ! grep -q id:999999 "${setup_conf_path}"; then
  cat <<EOF >> "${setup_conf_path}"


# Write the value from the X-CRS-Test header as a marker to the log
SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \\
    "id:999999,\\
    phase:1,\\
    pass,\\
    t:none,\\
    log,\\
    msg:'%{MATCHED_VAR}',\
    ctl:ruleRemoveById=1-999999"
EOF
fi

printf -- "- - -\nFinished CRS rule configuration\n# # #\n\n"
