From 389bb68ff09fb24ce98dae3d932dc6136c6c3bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 30 Apr 2026 11:51:20 +0200 Subject: [PATCH 01/65] SOLR-17767: Add a Java Agent --- .../15868-java-security-manager.yml | 6 + dev-docs/security-agent.adoc | 107 +++++ .../randomization/policies/solr-tests.policy | 4 + gradle/validation/rat-sources.gradle | 6 + settings.gradle | 1 + solr/agent-sm/build.gradle | 92 ++++ solr/agent-sm/gradle.lockfile | 167 ++++++++ .../solr/security/agent/ApprovedCallSite.java | 81 ++++ .../solr/security/agent/ExitInterceptor.java | 92 ++++ .../security/agent/FileAccessInterceptor.java | 185 ++++++++ .../agent/NetworkAccessInterceptor.java | 152 +++++++ .../security/agent/PermittedEndpoint.java | 71 ++++ .../solr/security/agent/PermittedPath.java | 72 ++++ .../solr/security/agent/PolicyLoader.java | 397 ++++++++++++++++++ .../agent/ProcessExecInterceptor.java | 92 ++++ .../agent/SecurityViolationLogger.java | 127 ++++++ .../security/agent/SolrAgentEntryPoint.java | 202 +++++++++ .../security/agent/SolrSecurityPolicy.java | 222 ++++++++++ .../solr/security/agent/StackInspector.java | 158 +++++++ .../security/agent/SuppressForbidden.java | 37 ++ .../agent/ViolationMetricsReporter.java | 209 +++++++++ .../solr/security/agent/package-info.java | 43 ++ .../security/agent/ExitInterceptorTest.java | 107 +++++ .../agent/NetworkAccessInterceptorTest.java | 106 +++++ .../PolicyLoaderOperatorExtensionTest.java | 216 ++++++++++ .../solr/security/agent/PolicyLoaderTest.java | 254 +++++++++++ .../agent/ProcessExecInterceptorTest.java | 100 +++++ .../agent/SolrAgentIntegrationTest.java | 184 ++++++++ .../security/agent/SymlinkEscapeTest.java | 167 ++++++++ .../security/agent/UncPathRejectionTest.java | 79 ++++ .../agent/VirtualThreadCompatibilityTest.java | 215 ++++++++++ solr/bin/solr | 20 +- solr/bin/solr.cmd | 13 + solr/bin/solr.in.cmd | 17 + solr/bin/solr.in.sh | 20 + solr/core/gradle.lockfile | 346 +++++++-------- .../org/apache/solr/core/CoreContainer.java | 15 + .../java/org/apache/solr/core/SolrPaths.java | 3 + solr/server/etc/agent-security-extra.policy | 54 +++ solr/server/etc/agent-security.policy | 104 +++++ solr/server/etc/security.policy | 5 + solr/solr-ref-guide/gradle.lockfile | 2 +- .../pages/solr-properties.adoc | 6 + .../deployment-guide/deployment-nav.adoc | 1 + .../deployment-guide/pages/securing-solr.adoc | 8 + .../pages/security-agent.adoc | 168 ++++++++ .../pages/indexing-with-tika.adoc | 19 + solr/solrj-zookeeper/gradle.lockfile | 292 ++++++------- solr/solrj/gradle.lockfile | 7 + solr/test-framework/gradle.lockfile | 300 ++++++------- .../checklists/requirements.md | 36 ++ .../contracts/policy-file-format.md | 140 ++++++ specs/001-jsm-replacement/data-model.md | 131 ++++++ specs/001-jsm-replacement/plan.md | 220 ++++++++++ specs/001-jsm-replacement/quickstart.md | 106 +++++ specs/001-jsm-replacement/research.md | 241 +++++++++++ specs/001-jsm-replacement/spec.md | 190 +++++++++ specs/001-jsm-replacement/tasks.md | 271 ++++++++++++ 58 files changed, 6215 insertions(+), 471 deletions(-) create mode 100644 changelog/unreleased/15868-java-security-manager.yml create mode 100644 dev-docs/security-agent.adoc create mode 100644 solr/agent-sm/build.gradle create mode 100644 solr/agent-sm/gradle.lockfile create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/ExitInterceptor.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/FileAccessInterceptor.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/NetworkAccessInterceptor.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedEndpoint.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/SolrSecurityPolicy.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/StackInspector.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/SuppressForbidden.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java create mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/ExitInterceptorTest.java create mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/NetworkAccessInterceptorTest.java create mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java create mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java create mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java create mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java create mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java create mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/UncPathRejectionTest.java create mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/VirtualThreadCompatibilityTest.java create mode 100644 solr/server/etc/agent-security-extra.policy create mode 100644 solr/server/etc/agent-security.policy create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc create mode 100644 specs/001-jsm-replacement/checklists/requirements.md create mode 100644 specs/001-jsm-replacement/contracts/policy-file-format.md create mode 100644 specs/001-jsm-replacement/data-model.md create mode 100644 specs/001-jsm-replacement/plan.md create mode 100644 specs/001-jsm-replacement/quickstart.md create mode 100644 specs/001-jsm-replacement/research.md create mode 100644 specs/001-jsm-replacement/spec.md create mode 100644 specs/001-jsm-replacement/tasks.md diff --git a/changelog/unreleased/15868-java-security-manager.yml b/changelog/unreleased/15868-java-security-manager.yml new file mode 100644 index 000000000000..b7e82b16ecdb --- /dev/null +++ b/changelog/unreleased/15868-java-security-manager.yml @@ -0,0 +1,6 @@ +title: > + SOLR-17767: Introduce a Java agent (solr-agent-sm) that enforces file access, outbound network, + System.exit(), and process-spawn controls via ByteBuddy instrumentation. +type: added +authors: + - name: Jan Høydahl diff --git a/dev-docs/security-agent.adoc b/dev-docs/security-agent.adoc new file mode 100644 index 000000000000..c97840518183 --- /dev/null +++ b/dev-docs/security-agent.adoc @@ -0,0 +1,107 @@ += Security Agent Developer Guide +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +This document is for Solr developers who need to work with or extend the `solr/agent-sm` security agent. + +== Module Overview + +`solr/agent-sm` is a standalone Gradle subproject that produces a Java agent JAR. +The agent must be a standalone JAR (not a Solr module) because it is loaded by the JVM before Solr initializes. +It intercepts bytecode using ByteBuddy and enforces a policy derived from JDK-style `.policy` files. + +Key classes: + +* `SolrAgentEntryPoint` — `premain()` and `agentmain()` entry points; wires all ByteBuddy interceptors +* `PolicyLoader` — parses `.policy` files and performs variable substitution +* `SolrSecurityPolicy` — immutable singleton holding the merged policy +* `StackInspector` — `StackWalker`-based call chain analysis (virtual-thread safe) +* `SecurityViolationLogger` — emits structured SLF4J log entries for violations +* `ViolationMetricsReporter` — `LongAdder` counters per violation type; deferred registration with `SolrMetricManager` + +== Adding an Approved Exit Caller + +To allow a new class to call `System.exit()` or `Runtime.halt()`, add it to the approved callers list in `SolrSecurityPolicy`. +The list is populated from the policy file using `java.lang.RuntimePermission "exitVM"` entries. +Add an entry to `agent-security.policy`: + +[source,text] +---- +grant codeBase "file:${solr.install.dir}/..." { + permission java.lang.RuntimePermission "exitVM"; +}; +---- + +Alternatively, add the class name to the default approved list in `SolrSecurityPolicy` if it is always a trusted Solr internal class. + +== Adding an Approved Exec Caller + +The default exec allow-list is empty — `ProcessBuilder.start()` and `Runtime.exec()` are blocked for all callers. +To permit a specific class, add a `java.lang.RuntimePermission "exec"` entry to the policy or the `SolrSecurityPolicy` defaults. +Document the reason in a code comment. + +== Adding a Trusted Filesystem Scheme + +Paths on certain virtual filesystems (e.g., `jrt:/` for JDK resources) are excluded from file policy checks. +To add a trusted scheme, extend the `trustedFileSystems` set in `SolrSecurityPolicy`. +Trusted schemes bypass all file access checks. + +== Deferred Metrics Registration + +The agent starts before Solr and begins counting violations immediately via `ViolationMetricsReporter`. +Once `CoreContainer` initializes, it calls `ViolationMetricsReporter.registerWithSolrMetrics()` via reflection (so `solr:core` has no compile-time dependency on `solr:agent-sm`). +Counts accumulated before registration are preserved in the `LongAdder` instances and become visible in metrics after registration. + +If you change the `registerWithSolrMetrics` method signature, update the reflective call in `CoreContainer.java` to match. + +== Writing Tests Alongside the Agent + +Tests for `solr:agent-sm` extend `SolrTestCase` (not `SolrTestCaseJ4`). + +The agent-sm test suite runs with `SOLR_SECURITY_AGENT_MODE=enforce` so that blocking behaviour is tested directly. +The broader Solr test suite runs in `warn` mode — violations are logged but do not fail tests. + +To run only the agent-sm tests: + +[source,bash] +---- +./gradlew :solr:agent-sm:test +---- + +To reproduce a flaky test with a specific randomization seed: + +[source,bash] +---- +./gradlew :solr:agent-sm:test --tests "org.apache.solr.security.agent.SolrAgentIntegrationTest" -Ptests.seed=HEXSEEDHERE +---- + +== Flipping the Broader Test Suite to Enforce Mode + +The broader Solr test suite currently runs in warn mode. +Before flipping to enforce mode: + +1. Run the full suite in warn mode and collect all `SECURITY VIOLATION` log entries. +2. Triage each violation — legitimate Solr operations need policy entries; plugin violations need plugin fixes or policy entries. +3. Update `agent-security.policy` or test-specific extra-policy files as needed. +4. File a JIRA (child of SOLR-17767) tracking this work before flipping. + +== No Compile-Time Dependency from solr:core on solr:agent-sm + +`solr:core` must not declare a compile-time dependency on `solr:agent-sm`. +The agent JAR is loaded by the JVM bootstrap classloader before application classloaders exist. +All interaction from `solr:core` must go through reflection (`Class.forName` with bootstrap classloader). +See `CoreContainer.java` for the pattern. diff --git a/gradle/testing/randomization/policies/solr-tests.policy b/gradle/testing/randomization/policies/solr-tests.policy index fe21f45a04c3..d3784039f997 100644 --- a/gradle/testing/randomization/policies/solr-tests.policy +++ b/gradle/testing/randomization/policies/solr-tests.policy @@ -15,6 +15,10 @@ * limitations under the License. */ +// NOTE: The JSM policy sections in this file are no longer enforced by the JVM — the Java Security +// Manager API was removed in JDK 24. This file is retained for use by the randomized test +// framework infrastructure and will be reviewed for removal in a future release. + // Policy file for solr. Please keep minimal and avoid wildcards. // permissions needed for tests to pass, based on properties set by the build system diff --git a/gradle/validation/rat-sources.gradle b/gradle/validation/rat-sources.gradle index f8556d2ffa4f..beb3efa01802 100644 --- a/gradle/validation/rat-sources.gradle +++ b/gradle/validation/rat-sources.gradle @@ -92,6 +92,12 @@ allprojects { // Exclude github stuff (templates, workflows). exclude ".github" + // Exclude speckit planning artifacts and Claude Code tooling. + exclude "specs/**" + exclude ".claude/**" + exclude ".specify/**" + exclude "CLAUDE.md" + exclude "dev-tools/scripts/cloud.sh" exclude "dev-tools/scripts/README.md" exclude "dev-tools/scripts/create_line_file_docs.py" diff --git a/settings.gradle b/settings.gradle index 782edec43251..a8f48ac9883d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -36,6 +36,7 @@ rootProject.name = "solr-root" includeBuild("build-tools/missing-doclet") include ":platform" +include "solr:agent-sm" include "solr:api" include "solr:solrj" include "solr:solrj-jetty" diff --git a/solr/agent-sm/build.gradle b/solr/agent-sm/build.gradle new file mode 100644 index 000000000000..0b5be99e5d96 --- /dev/null +++ b/solr/agent-sm/build.gradle @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'java-library' + +description = 'Apache Solr Security Agent (Java Security Manager replacement)' + +dependencies { + // ByteBuddy references SpotBugs annotations in its class files; needed to suppress -Xlint:classfile + // Not included in the release so exclude from license/checksum checks. + compileOnly libs.spotbugs.annotations + + // ByteBuddy for bytecode instrumentation - bundled into the fat agent JAR + implementation libs.bytebuddy + // byte-buddy-agent is the Java agent JAR itself; not directly imported in compiled classes + // but required at runtime for agent loading and bundled into the fat JAR. + implementation libs.bytebuddy.agent + permitUnusedDeclared libs.bytebuddy.agent + + // SLF4J API for violation logging - bundled; implementation loaded via context classloader + implementation libs.slf4j.api + + // Test dependencies - NOT bundled + testImplementation project(':solr:test-framework') + testImplementation project(':solr:core') + testImplementation libs.apache.lucene.testframework + testImplementation libs.junit.junit + testImplementation libs.carrotsearch.randomizedtesting.runner + testImplementation libs.hamcrest.hamcrest + // These are used indirectly via SolrTestCase (test-framework) and randomized runner base classes; + // the static analyzer does not see direct usage in agent-sm test sources. + permitTestUnusedDeclared project(':solr:core') + permitTestUnusedDeclared libs.apache.lucene.testframework + permitTestUnusedDeclared libs.carrotsearch.randomizedtesting.runner + permitTestUnusedDeclared libs.hamcrest.hamcrest +} + +// Build a self-contained fat JAR so the agent can run before Solr's classpath is set up. +// ByteBuddy and SLF4J API are bundled; Solr classes are NOT included. +jar { + manifest { + attributes( + 'Premain-Class': 'org.apache.solr.security.agent.SolrAgentEntryPoint', + 'Agent-Class': 'org.apache.solr.security.agent.SolrAgentEntryPoint', + 'Can-Redefine-Classes': 'true', + 'Can-Retransform-Classes': 'true' + ) + } + + // Bundle runtime dependencies (ByteBuddy, SLF4J API) into the fat agent JAR. + // Test dependencies are excluded because they are not on runtimeClasspath. + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +// Copy the built agent JAR to server/lib/ext/ so startup scripts can find it at +// ${SOLR_SERVER_DIR}/lib/ext/solr-agent-sm-*.jar, and so the dev distribution works. +def agentJarTarget = file("${rootDir}/solr/server/lib/ext") + +task copyAgentJar(type: Copy, dependsOn: jar) { + description = 'Copies the agent fat JAR to solr/server/lib/ext/ for startup-script detection' + group = 'build' + from jar.outputs + into agentJarTarget + // Keep only the current version; remove stale versions on each build. + doFirst { + delete fileTree(agentJarTarget) { include 'solr-agent-sm-*.jar' } + } +} + +// spotbugs-annotations is compileOnly and not included in the release. +configurations.jarValidation { + exclude group: "com.github.spotbugs", module: "spotbugs-annotations" +} + +assemble.dependsOn copyAgentJar diff --git a/solr/agent-sm/gradle.lockfile b/solr/agent-sm/gradle.lockfile new file mode 100644 index 000000000000..4dbbe046cd7d --- /dev/null +++ b/solr/agent-sm/gradle.lockfile @@ -0,0 +1,167 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.4=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.carrotsearch:hppc:0.10.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.core:jackson-annotations:2.21=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.core:jackson-core:2.21.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.core:jackson-databind:2.21.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.21.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.21.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.21.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson:jackson-bom:2.21.2=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.woodstox:woodstox-core:7.0.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,errorprone,jarValidation,permitTestUnusedDeclared,testAnnotationProcessor,testRuntimeClasspath,testRuntimeClasspathCopy +com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,errorprone,testAnnotationProcessor +com.github.spotbugs:spotbugs-annotations:4.9.8=compileClasspath,compileOnlyHelper +com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,errorprone,testAnnotationProcessor +com.google.auto.value:auto-value-annotations:1.11.0=annotationProcessor,errorprone,testAnnotationProcessor +com.google.auto:auto-common:1.2.2=annotationProcessor,errorprone,testAnnotationProcessor +com.google.errorprone:error_prone_annotation:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor +com.google.errorprone:error_prone_annotations:2.41.0=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.google.errorprone:error_prone_annotations:2.43.0=annotationProcessor,errorprone,testAnnotationProcessor +com.google.errorprone:error_prone_check_api:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor +com.google.errorprone:error_prone_core:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor +com.google.googlejavaformat:google-java-format:1.27.0=annotationProcessor,errorprone,testAnnotationProcessor +com.google.guava:failureaccess:1.0.3=annotationProcessor,errorprone,jarValidation,permitTestUnusedDeclared,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.google.guava:guava:33.5.0-jre=annotationProcessor,errorprone,jarValidation,permitTestUnusedDeclared,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,errorprone,jarValidation,permitTestUnusedDeclared,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,errorprone,jarValidation,permitTestUnusedDeclared,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.google.protobuf:protobuf-java:3.25.8=annotationProcessor,errorprone,testAnnotationProcessor +com.j256.simplemagic:simplemagic:1.17=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +com.jayway.jsonpath:json-path:2.9.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +com.tdunning:t-digest:3.3=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +commons-cli:commons-cli:1.11.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +commons-codec:commons-codec:1.21.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +commons-io:commons-io:2.21.0=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.dropwizard.metrics:metrics-core:3.2.5=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,errorprone,testAnnotationProcessor +io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,errorprone,testAnnotationProcessor +io.netty:netty-buffer:4.2.12.Final=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-codec-base:4.2.12.Final=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-common:4.2.12.Final=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-handler:4.2.12.Final=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-resolver:4.2.12.Final=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-tcnative-boringssl-static:2.0.75.Final=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-tcnative-classes:2.0.75.Final=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-transport-classes-epoll:4.2.12.Final=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-transport-native-epoll:4.2.12.Final=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-transport-native-unix-common:4.2.12.Final=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-transport:4.2.12.Final=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator:2.22.0-alpha=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:2.22.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java17:2.22.0-alpha=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java8:2.22.0-alpha=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.semconv:opentelemetry-semconv:1.37.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-api-incubator:1.56.0-alpha=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-api:1.56.0=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-common:1.56.0=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-context:1.56.0=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-exporter-prometheus:1.56.0-alpha=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk-common:1.56.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk-metrics:1.56.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk-trace:1.56.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk:1.56.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +io.prometheus:prometheus-metrics-exposition-formats:1.1.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +io.prometheus:prometheus-metrics-model:1.1.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +io.sgr:s2-geometry-library-java:1.0.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +io.swagger.core.v3:swagger-annotations-jakarta:2.2.22=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.activation:jakarta.activation-api:2.1.3=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.annotation:jakarta.annotation-api:3.0.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.inject:jakarta.inject-api:2.0.1=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.servlet:jakarta.servlet-api:6.1.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.validation:jakarta.validation-api:3.1.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor +junit:junit:4.13.2=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +net.bytebuddy:byte-buddy-agent:1.18.8-jdk5=compileClasspath,compileClasspathCopy,jarValidation,permitUnusedDeclared,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +net.bytebuddy:byte-buddy:1.18.8-jdk5=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.antlr:antlr4-runtime:4.13.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.commons:commons-exec:1.6.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.commons:commons-lang3:3.20.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.commons:commons-math3:3.6.1=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.curator:curator-client:5.9.0=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.curator:curator-framework:5.9.0=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.curator:curator-test:5.9.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.httpcomponents:httpclient:4.5.14=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.httpcomponents:httpcore:4.4.16=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.httpcomponents:httpmime:4.5.14=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.logging.log4j:log4j-api:2.25.3=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.logging.log4j:log4j-core:2.25.3=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.logging.log4j:log4j-slf4j2-impl:2.25.3=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-common:10.4.0=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-kuromoji:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-nori:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-phonetic:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-backward-codecs:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-classification:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-codecs:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-core:10.4.0=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-expressions:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-facet:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-grouping:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-highlighter:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-join:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-memory:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-misc:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-queries:10.4.0=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-queryparser:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-sandbox:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-spatial-extras:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-spatial3d:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-suggest:10.4.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-test-framework:10.4.0=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.zookeeper:zookeeper-jute:3.9.4=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.zookeeper:zookeeper:3.9.4=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apiguardian:apiguardian-api:1.1.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.codehaus.woodstox:stax2-api:4.2.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-client-transport:12.0.34=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-client:12.0.34=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-common:12.0.34=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-hpack:12.0.34=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-server:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-client:12.0.34=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-java-client:12.0.34=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-java-server:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-server:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-client:12.0.34=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-http:12.0.34=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-io:12.0.34=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-rewrite:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-security:12.0.34=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-server:12.0.34=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-session:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-util:12.0.34=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2.external:aopalliance-repackaged:4.0.0-M3=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2:hk2-api:4.0.0-M3=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2:hk2-locator:4.0.0-M3=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2:hk2-utils:4.0.0-M3=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2:osgi-resource-locator:3.0.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.containers:jersey-container-jetty-http:4.0.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.core:jersey-client:4.0.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.core:jersey-common:4.0.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.core:jersey-server:4.0.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.ext:jersey-entity-filtering:4.0.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.inject:jersey-hk2:4.0.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.media:jersey-media-json-jackson:4.0.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey:jersey-bom:4.0.2=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.hamcrest:hamcrest:3.0=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.javassist:javassist:3.30.2-GA=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.jspecify:jspecify:1.0.0=annotationProcessor,errorprone,jarValidation,permitTestUnusedDeclared,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.junit.jupiter:junit-jupiter-api:5.6.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.junit.platform:junit-platform-commons:1.6.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.junit:junit-bom:5.14.0=compileOnlyHelper +org.junit:junit-bom:5.6.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.locationtech.spatial4j:spatial4j:0.8=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.opentest4j:opentest4j:1.2.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.ow2.asm:asm-commons:9.8=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.ow2.asm:asm-tree:9.8=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.ow2.asm:asm:9.8=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.pcollections:pcollections:4.0.1=annotationProcessor,errorprone,testAnnotationProcessor +org.semver4j:semver4j:6.0.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.slf4j:jcl-over-slf4j:2.0.17=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +org.slf4j:slf4j-api:2.0.17=compileClasspath,compileClasspathCopy,jarValidation,permitTestUnusedDeclared,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.xerial.snappy:snappy-java:1.1.10.8=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy +empty=apiHelper,apiHelperTest,compileOnlyHelperTest,missingdoclet,permitAggregatorUse,permitTestAggregatorUse,permitTestUsedUndeclared,permitUsedUndeclared diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java new file mode 100644 index 000000000000..bfbc20f88ffd --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +/** + * A class (or class-name prefix pattern) that is approved to perform a restricted operation such as + * calling {@code System.exit()} or spawning a child process via {@code ProcessBuilder}. + * + *

Approved call sites are loaded from the security policy at startup and are immutable. Entries + * use either an exact fully-qualified class name or a prefix ending in {@code .*} (e.g. {@code + * org.apache.solr.cli.*}). + * + *

Default approved EXIT callers: + * + *

+ * + *

Default approved EXEC callers: none (empty list in production policy). + */ +public final class ApprovedCallSite { + + /** The restricted operation covered by this approval. */ + public enum Operation { + EXIT, + EXEC + } + + private final String classNamePattern; + private final Operation operation; + private final PolicyLoader.PolicySource source; + + ApprovedCallSite(String classNamePattern, Operation operation, PolicyLoader.PolicySource source) { + this.classNamePattern = classNamePattern; + this.operation = operation; + this.source = source; + } + + /** Fully-qualified class name or prefix pattern (ending in {@code .*}). */ + public String classNamePattern() { + return classNamePattern; + } + + /** The restricted operation this approval covers. */ + public Operation operation() { + return operation; + } + + /** Whether this entry came from the default bundled policy or an operator extension. */ + public PolicyLoader.PolicySource source() { + return source; + } + + /** + * Returns {@code true} if the given fully-qualified class name matches this approved call-site + * pattern. + */ + public boolean matches(String className) { + if ("*".equals(classNamePattern)) return true; + if (classNamePattern.endsWith(".*")) { + String prefix = classNamePattern.substring(0, classNamePattern.length() - 2); + return className.equals(prefix) || className.startsWith(prefix + "."); + } + return classNamePattern.equals(className); + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ExitInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ExitInterceptor.java new file mode 100644 index 000000000000..540cf2dade7f --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ExitInterceptor.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import net.bytebuddy.asm.Advice; + +/** + * ByteBuddy {@link Advice} interceptor for {@code System.exit()} and {@code Runtime.halt()}. + * + *

Checks the top caller class against the {@link SolrSecurityPolicy#approvedExitCallers()} list. + * If the caller is not approved, the violation is logged and (in enforce mode) a {@link + * SecurityException} is thrown, preventing the JVM from terminating. + * + *

Default approved callers (from the bundled default policy): + * + *

+ * + *

Operators may add additional approved callers via {@code agent-security-extra.policy} using a + * codeBase-scoped {@code RuntimePermission "exitVM"} grant. + */ +public final class ExitInterceptor { + + private ExitInterceptor() {} + + /** + * Called before {@code System.exit(int)}. + * + * @param status the exit status code (unused for the policy check) + */ + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onSystemExit(@Advice.Argument(0) int status) { + checkExit("System.exit(" + status + ")"); + } + + /** + * Called before {@code Runtime.halt(int)}. + * + * @param status the halt status code (unused for the policy check) + */ + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onRuntimeHalt(@Advice.Argument(0) int status) { + checkExit("Runtime.halt(" + status + ")"); + } + + // --------------------------------------------------------------------------- + // Core check logic + // --------------------------------------------------------------------------- + + /** + * Checks whether the current top caller is approved to call {@code System.exit()} or {@code + * Runtime.halt()}. Delegates to {@link SecurityViolationLogger} on violation. + * + * @param target a human-readable description of the intercepted call for the violation log + */ + static void checkExit(String target) { + if (!SolrSecurityPolicy.isInitialized()) return; + + SolrSecurityPolicy policy = SolrSecurityPolicy.getInstance(); + String caller = StackInspector.topCallerClassName(); + + if (!policy.isExitApproved(caller)) { + ViolationMetricsReporter.incrementExit(); + SecurityViolationLogger.log( + SecurityViolationLogger.ViolationType.SYSTEM_EXIT, + target, + caller, + policy.enforcementMode()); + if (policy.enforcementMode() == SolrSecurityPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException( + "System.exit() / Runtime.halt() denied by Solr security agent — unapproved caller: " + + caller); + } + } + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileAccessInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileAccessInterceptor.java new file mode 100644 index 000000000000..6dc86037c609 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileAccessInterceptor.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import net.bytebuddy.asm.Advice; + +/** + * ByteBuddy {@link Advice} interceptor for file-system operations. + * + *

This class is injected into the bootstrap classloader via {@link + * net.bytebuddy.dynamic.loading.ClassInjector.UsingUnsafe} so that it can intercept JDK methods. + * Every public method annotated with {@code Advice.OnMethodEnter} runs before the intercepted + * method body. + * + *

Enforcement

+ * + *
    + *
  1. Windows UNC paths ({@code \\host\share\...}) are rejected unconditionally regardless of any + * policy rule (FR-003). + *
  2. Symlinks are resolved to their real path via {@code toRealPath()} before the policy check, + * preventing symlink-escape attacks (FR-004). + *
  3. The resolved path is checked against {@link SolrSecurityPolicy#permittedPaths()} for the + * relevant action. + *
  4. Violations are handed to {@link SecurityViolationLogger} and, in enforce mode, result in a + * {@link SecurityException} being thrown. + *
+ * + *

Trusted filesystems

+ * + * Paths on filesystem schemes listed in {@link SolrSecurityPolicy#trustedFileSystems()} (e.g. + * in-memory filesystems used by tests) are exempt from enforcement. + */ +public final class FileAccessInterceptor { + + private FileAccessInterceptor() {} + + /** + * Called before any NIO file-read operation. Checks the resolved path against the policy. + * + * @param path the {@link Path} argument of the intercepted method + */ + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onFileRead(@Advice.Argument(0) Path path) { + checkPath(path, "read", SecurityViolationLogger.ViolationType.FILE_READ); + } + + /** + * Called before any NIO file-write or create operation. + * + * @param path the {@link Path} argument of the intercepted method + */ + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onFileWrite(@Advice.Argument(0) Path path) { + checkPath(path, "write", SecurityViolationLogger.ViolationType.FILE_WRITE); + } + + /** + * Called before any NIO file-delete operation. + * + * @param path the {@link Path} argument of the intercepted method + */ + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onFileDelete(@Advice.Argument(0) Path path) { + checkPath(path, "delete", SecurityViolationLogger.ViolationType.FILE_DELETE); + } + + // --------------------------------------------------------------------------- + // Core check logic — shared by all three entry points + // --------------------------------------------------------------------------- + + /** + * Performs the UNC check, symlink resolution, and policy lookup for a given path and action. + * Delegates to {@link SecurityViolationLogger} on violation, and (in enforce mode) throws {@link + * SecurityException}. + */ + static void checkPath(Path path, String action, SecurityViolationLogger.ViolationType type) { + if (path == null) return; + + // Check if the policy singleton is available yet; skip if not (very early startup). + if (!SolrSecurityPolicy.isInitialized()) return; + + SolrSecurityPolicy policy = SolrSecurityPolicy.getInstance(); + + // Skip trusted filesystem schemes (e.g. in-memory FS used by tests). + String scheme = path.toUri().getScheme(); + if (scheme != null && policy.trustedFileSystems().contains(scheme)) return; + + String rawPathStr = path.toAbsolutePath().toString(); + + // FR-003: Block Windows UNC paths unconditionally on all platforms. + if (isUncPath(rawPathStr)) { + String caller = StackInspector.topCallerClassName(); + ViolationMetricsReporter.incrementFile(); + SecurityViolationLogger.log( + SecurityViolationLogger.ViolationType.FILE_READ, + rawPathStr, + caller, + policy.enforcementMode(), + "UNC_BLOCKED"); + if (policy.enforcementMode() == SolrSecurityPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException("UNC path access denied by Solr security agent: " + rawPathStr); + } + return; + } + + // FR-004: Resolve symlinks before the policy check. + String resolvedPathStr = resolveRealPath(path, rawPathStr); + + // Check against policy. + if (!policy.isPathPermitted(resolvedPathStr, action)) { + String caller = StackInspector.topCallerClassName(); + ViolationMetricsReporter.incrementFile(); + SecurityViolationLogger.log(type, resolvedPathStr, caller, policy.enforcementMode()); + if (policy.enforcementMode() == SolrSecurityPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException( + "File " + action + " denied by Solr security agent: " + resolvedPathStr); + } + } + } + + /** String-based overload for legacy {@link java.io.File} paths. */ + static void checkPath(String pathStr, String action, SecurityViolationLogger.ViolationType type) { + if (pathStr == null || pathStr.isEmpty()) return; + checkPath(Path.of(pathStr), action, type); + } + + /** + * Returns {@code true} if the given path string is a Windows UNC path ({@code \\host\share\...} + * or the forward-slash equivalent {@code //host/share/...}). This check is platform-independent + * so UNC paths are rejected on Linux and macOS too. + */ + static boolean isUncPath(String pathStr) { + return pathStr.startsWith("\\\\") || pathStr.startsWith("//"); + } + + /** + * Resolves the real (symlink-free) path. Returns the original path string if {@code toRealPath()} + * throws an {@link IOException} (e.g. the file does not yet exist — a pre-create check) or if a + * {@code SecurityException} is thrown (e.g. by the Java SecurityManager in the test environment). + * The unresolved path string is still subject to the policy check. + */ + private static String resolveRealPath(Path path, String fallback) { + try { + return path.toRealPath().toString(); + } catch (IOException | SecurityException e) { + // File may not exist yet (pre-create), or access is restricted; use the normalized path. + return path.normalize().toAbsolutePath().toString(); + } + } + + // --------------------------------------------------------------------------- + // Legacy java.io.File path interception helper + // --------------------------------------------------------------------------- + + /** + * Entry point for {@code java.io.File}-based operations (e.g. {@code FileInputStream}, {@code + * FileOutputStream}). Converts the {@code File} to a {@code Path} and delegates. + */ + @SuppressForbidden( + reason = + "java.io.File is the parameter type of the intercepted legacy JDK method " + + "(e.g. FileInputStream(File)); the Advice method signature must match.") + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onFileIo(@Advice.Argument(0) File file) { + if (file == null) return; + checkPath(file.toPath(), "read", SecurityViolationLogger.ViolationType.FILE_READ); + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/NetworkAccessInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/NetworkAccessInterceptor.java new file mode 100644 index 000000000000..c6451777b5e0 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/NetworkAccessInterceptor.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import net.bytebuddy.asm.Advice; + +/** + * ByteBuddy {@link Advice} interceptor for outbound network connections. + * + *

Intercepts {@code SocketChannel.connect(SocketAddress)} and {@code + * Socket.connect(SocketAddress)} to enforce the {@link SolrSecurityPolicy#permittedEndpoints()} + * list. Loopback addresses are unconditionally allowed by this interceptor regardless of policy + * entries, as a safety net against policy misconfiguration. + * + *

Port-wildcard entries in the default policy (e.g. {@code *:8983}) are matched by comparing + * only the port number when the host portion of the entry is {@code *}. + */ +public final class NetworkAccessInterceptor { + + private NetworkAccessInterceptor() {} + + /** + * Called before {@code SocketChannel.connect()} or {@code Socket.connect()}. + * + * @param address the remote address to connect to + */ + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onConnect(@Advice.Argument(0) SocketAddress address) { + if (!(address instanceof InetSocketAddress)) return; + checkConnect((InetSocketAddress) address); + } + + // --------------------------------------------------------------------------- + // Core check logic + // --------------------------------------------------------------------------- + + /** + * Checks the given resolved remote address against the active policy's permitted endpoint list. + * Loopback and unresolved addresses are allowed unconditionally. + */ + static void checkConnect(InetSocketAddress address) { + if (!SolrSecurityPolicy.isInitialized()) return; + if (address.isUnresolved()) return; + + InetAddress inetAddress = address.getAddress(); + if (inetAddress != null && inetAddress.isLoopbackAddress()) return; + + SolrSecurityPolicy policy = SolrSecurityPolicy.getInstance(); + int port = address.getPort(); + String host = inetAddress != null ? inetAddress.getHostAddress() : address.getHostName(); + + if (!isEndpointPermitted(policy, host, port)) { + String target = host + ":" + port; + String caller = StackInspector.topCallerClassName(); + ViolationMetricsReporter.incrementNetwork(); + SecurityViolationLogger.log( + SecurityViolationLogger.ViolationType.NETWORK_CONNECT, + target, + caller, + policy.enforcementMode()); + if (policy.enforcementMode() == SolrSecurityPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException( + "Outbound network connection denied by Solr security agent: " + target); + } + } + } + + /** + * Returns {@code true} if at least one permitted endpoint entry in the policy covers the given + * host and port. Matching rules: + * + *

+ * + *

CodBase-scoped entries ({@link PermittedEndpoint#codeBase()} non-null) are skipped in this + * path-based check; they are handled by the JVM's own permission system for code loaded from the + * specified location. + */ + static boolean isEndpointPermitted(SolrSecurityPolicy policy, String host, int port) { + for (PermittedEndpoint entry : policy.permittedEndpoints()) { + // Skip codeBase-scoped entries; they are handled separately at the module level. + if (entry.codeBase() != null) continue; + + String hostPort = entry.hostPort(); + if (matchesEndpoint(hostPort, host, port)) return true; + } + return false; + } + + private static boolean matchesEndpoint(String hostPortEntry, String host, int port) { + if ("*".equals(hostPortEntry)) return true; // broad wildcard + + int colonIdx = hostPortEntry.lastIndexOf(':'); + if (colonIdx < 0) { + // host-only entry — matches any port on that host + return matchesHost(hostPortEntry, host); + } + + String entryHost = hostPortEntry.substring(0, colonIdx); + String entryPort = hostPortEntry.substring(colonIdx + 1); + + if (!matchesHost(entryHost, host)) return false; + + return matchesPort(entryPort, port); + } + + private static boolean matchesHost(String entryHost, String actualHost) { + if ("*".equals(entryHost)) return true; + return entryHost.equalsIgnoreCase(actualHost); + } + + private static boolean matchesPort(String entryPort, int actualPort) { + if ("*".equals(entryPort)) return true; + int dashIdx = entryPort.indexOf('-'); + if (dashIdx < 0) { + try { + return Integer.parseInt(entryPort.trim()) == actualPort; + } catch (NumberFormatException e) { + return false; + } + } + // Range: low-high + try { + int low = Integer.parseInt(entryPort.substring(0, dashIdx).trim()); + int high = Integer.parseInt(entryPort.substring(dashIdx + 1).trim()); + return actualPort >= low && actualPort <= high; + } catch (NumberFormatException e) { + return false; + } + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedEndpoint.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedEndpoint.java new file mode 100644 index 000000000000..08a04f20a383 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedEndpoint.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +/** + * A single outbound network access rule from the security policy. + * + *

Entries are loaded at startup and are immutable thereafter. An entry permits outbound + * connections to a host-and-port pair. The {@code hostPort} string follows JDK {@code + * SocketPermission} syntax: {@code "host:port"}, {@code "*:8983"} (wildcard host), {@code + * "host:1-65535"} (port range), etc. + * + *

An optional {@code codeBase} restricts the grant to code loaded from a specific JAR location. + * A {@code null} codeBase means the grant applies to all code. + */ +public final class PermittedEndpoint { + + private final String hostPort; + private final String actions; + private final String codeBase; // null → applies to all code + private final PolicyLoader.PolicySource source; + + PermittedEndpoint( + String hostPort, String actions, String codeBase, PolicyLoader.PolicySource source) { + this.hostPort = hostPort; + this.actions = actions != null ? actions : "connect,resolve"; + this.codeBase = codeBase; + this.source = source; + } + + /** + * Host-and-port string as written in the policy file (after variable substitution). Examples: + * {@code "localhost:8983"}, {@code "*:8983"}, {@code "127.0.0.1:1-65535"}. + */ + public String hostPort() { + return hostPort; + } + + /** Actions string, e.g. {@code "connect,resolve"}. */ + public String actions() { + return actions; + } + + /** + * The {@code codeBase} URL pattern this entry is scoped to, or {@code null} for a global grant. + * Used for pre-permitting bundled modules (e.g. jwt-auth) while keeping the grant out of reach of + * arbitrary code. + */ + public String codeBase() { + return codeBase; + } + + /** Whether this rule came from the default bundled policy or an operator extension. */ + public PolicyLoader.PolicySource source() { + return source; + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java new file mode 100644 index 000000000000..6602be6f4f70 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.util.Locale; + +/** + * A single file-system access rule from the security policy. + * + *

Rules are loaded at startup and are immutable thereafter. A rule grants access to a path (and + * optionally all descendants) for the set of operations encoded in the {@code actions} string + * ({@code "read"}, {@code "write"}, {@code "delete"}, or any comma-separated combination). + */ +public final class PermittedPath { + + private final String path; + private final String actions; + private final boolean recursive; + private final PolicyLoader.PolicySource source; + + PermittedPath(String path, String actions, boolean recursive, PolicyLoader.PolicySource source) { + this.path = path; + this.actions = actions != null ? actions.toLowerCase(Locale.ROOT) : "read"; + this.recursive = recursive; + this.source = source; + } + + /** The base path after variable substitution. */ + public String path() { + return path; + } + + /** Comma-separated actions string, lower-cased (e.g. {@code "read,write,delete"}). */ + public String actions() { + return actions; + } + + /** Whether the rule covers all descendants ({@code path/-} in policy syntax). */ + public boolean recursive() { + return recursive; + } + + /** Whether this rule came from the default bundled policy or an operator extension. */ + public PolicyLoader.PolicySource source() { + return source; + } + + /** Returns {@code true} if this rule permits the given action on the given resolved path. */ + public boolean permits(String resolvedPath, String action) { + boolean pathMatch = + recursive + ? resolvedPath.startsWith(path + "/") + || resolvedPath.startsWith(path + "\\") + || resolvedPath.equals(path) + : resolvedPath.equals(path); + return pathMatch && actions.contains(action.toLowerCase(Locale.ROOT)); + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java new file mode 100644 index 000000000000..4933d8433bbb --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java @@ -0,0 +1,397 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Reads and parses JDK-style {@code .policy} files, performing Solr-specific variable substitution, + * and produces a {@link SolrSecurityPolicy} ready for enforcement. + * + *

Variable substitution

+ * + * The following variables are expanded in permission targets and {@code codeBase} URLs before + * parsing: + * + * + * + *

Two-file merge

+ * + * Two files are loaded: the mandatory default policy and an optional operator extension file. The + * extra-policy file path is resolved from system property {@code solr.security.agent.extra.policy}, + * falling back to {@code ${server.dir}/etc/agent-security-extra.policy}. An absent extra-policy + * file is silently skipped. Each entry carries a {@link PermittedPath#source() source} tag of + * either {@link PolicySource#DEFAULT} or {@link PolicySource#OPERATOR}. + * + *

A missing or unparseable default policy causes an {@link IllegalStateException} at + * startup. + */ +public class PolicyLoader { + + /** The source that an entry came from — default bundled policy or operator extension. */ + public enum PolicySource { + DEFAULT, + OPERATOR + } + + // Variables expanded in path and codeBase expressions before parsing. + private static final String[] SYSTEM_VARS = { + "solr.home", + "solr.data.dir", + "solr.log.dir", + "solr.install.dir", + "java.io.tmpdir", + "java.home", + "user.home", + }; + + // Pattern to match a variable reference such as ${solr.home} + private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\{([^}]+)\\}"); + + /** + * Loads and merges the default policy file and the optional operator extension file. + * + * @param defaultPolicyPath absolute path to the default {@code agent-security.policy} file + * @return a fully initialized {@link SolrSecurityPolicy} + * @throws IllegalStateException if the default policy file is absent or cannot be parsed + */ + public SolrSecurityPolicy load(Path defaultPolicyPath) { + if (!Files.exists(defaultPolicyPath)) { + throw new IllegalStateException( + "Security agent default policy not found: " + + defaultPolicyPath + + ". Solr cannot start without a valid security policy. " + + "Check that agent-security.policy is present in server/etc/."); + } + + String defaultContent; + try { + defaultContent = Files.readString(defaultPolicyPath, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException( + "Failed to read security agent default policy: " + defaultPolicyPath, e); + } + + List grants = new ArrayList<>(); + parsePolicy(defaultContent, PolicySource.DEFAULT, grants); + + // Resolve extra-policy path: system property → fallback to ${server.dir}/etc/... + Path extraPolicyPath = resolveExtraPolicyPath(); + if (extraPolicyPath != null && Files.exists(extraPolicyPath)) { + String extraContent; + try { + extraContent = Files.readString(extraPolicyPath, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException( + "Failed to read operator security policy extension: " + extraPolicyPath, e); + } + parsePolicy(extraContent, PolicySource.OPERATOR, grants); + } + + return buildPolicy(grants); + } + + /** + * Resolves the extra-policy file path from system property {@code + * solr.security.agent.extra.policy}, falling back to {@code + * ${server.dir}/etc/agent-security-extra.policy}. Returns {@code null} if no fallback is + * available. + */ + static Path resolveExtraPolicyPath() { + String explicitPath = System.getProperty("solr.security.agent.extra.policy"); + if (explicitPath != null && !explicitPath.isBlank()) { + return Path.of(explicitPath); + } + String serverDir = System.getProperty("jetty.home", System.getProperty("server.dir")); + if (serverDir != null && !serverDir.isBlank()) { + return Path.of(serverDir, "etc", "agent-security-extra.policy"); + } + return null; + } + + /** + * Parses a policy file, substituting variables, and appends the resulting {@link GrantBlock} + * entries — tagged with the given {@code source} — to {@code out}. + */ + static void parsePolicy(String content, PolicySource source, List out) { + String expanded = substituteVariables(content); + parsePolicyBlocks(expanded, source, out); + } + + /** + * Expands all {@code ${variable}} references in {@code text}. Unknown variables are left as-is. + * {@code ${solr.port}} and {@code ${solr.zk.port}} receive special handling so that the ZK port + * defaults to {@code solr.port + 1000} when not explicitly set. + */ + static String substituteVariables(String text) { + // Resolve solr.port once so we can derive the default ZK port. + String solrPortStr = System.getProperty("solr.port", "8983"); + int solrPort; + try { + solrPort = Integer.parseInt(solrPortStr.trim()); + } catch (NumberFormatException e) { + solrPort = 8983; + } + String zkPortStr = System.getProperty("solr.zk.port"); + String zkPort = + (zkPortStr != null && !zkPortStr.isBlank()) + ? zkPortStr.trim() + : String.valueOf(solrPort + 1000); + + StringBuffer sb = new StringBuffer(); + Matcher m = VAR_PATTERN.matcher(text); + while (m.find()) { + String varName = m.group(1); + String replacement; + if ("solr.port".equals(varName)) { + replacement = solrPortStr.trim(); + } else if ("solr.zk.port".equals(varName)) { + replacement = zkPort; + } else { + replacement = resolveSystemVar(varName); + } + m.appendReplacement( + sb, Matcher.quoteReplacement(replacement != null ? replacement : m.group(0))); + } + m.appendTail(sb); + return sb.toString(); + } + + private static String resolveSystemVar(String varName) { + // Try system property first, then environment-style lookup. + String val = System.getProperty(varName); + if (val != null) return val; + // For well-known vars, also try without dots. + return null; + } + + /** + * Parses grant blocks from the given (already variable-substituted) policy text. Only the + * permission types used by the Solr agent are recognised: + * + *

+ */ + static void parsePolicyBlocks(String text, PolicySource source, List out) { + // Strip single-line comments + String noComments = stripComments(text); + + // Match: grant [codeBase "url"] { ... }; + Pattern grantPattern = + Pattern.compile( + "grant\\s*(?:codeBase\\s*\"([^\"]*?)\")?\\s*\\{([^}]*)\\}\\s*;", + Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + + Matcher grantMatcher = grantPattern.matcher(noComments); + while (grantMatcher.find()) { + String codeBase = grantMatcher.group(1); // null for global grants + String body = grantMatcher.group(2); + GrantBlock block = new GrantBlock(codeBase, source); + parsePermissions(body, block); + out.add(block); + } + } + + private static String stripComments(String text) { + // Remove // line comments; leave /* */ block comments as-is (not used in standard policy files) + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new StringReader(text))) { + String line; + while ((line = reader.readLine()) != null) { + int commentIdx = line.indexOf("//"); + if (commentIdx >= 0) { + line = line.substring(0, commentIdx); + } + sb.append(line).append('\n'); + } + } catch (IOException e) { + // StringReader never throws + throw new AssertionError(e); + } + return sb.toString(); + } + + /** + * Parses individual {@code permission} lines inside a grant block body and adds recognised + * permissions to the block. + */ + static void parsePermissions(String body, GrantBlock block) { + // permission ["target"] [, "actions"]; + Pattern permPattern = + Pattern.compile( + "permission\\s+(\\S+)\\s*(?:\"([^\"]*?)\")?\\s*(?:,\\s*\"([^\"]*?)\")?\\s*;", + Pattern.CASE_INSENSITIVE); + + Matcher m = permPattern.matcher(body); + while (m.find()) { + String permClass = m.group(1); + String target = m.group(2); // may be null + String actions = m.group(3); // may be null + + switch (permClass) { + case "java.io.FilePermission": + if (target != null) { + block.filePaths.add( + new RawFilePermission(target, actions != null ? actions : "read", block.source)); + } + break; + case "java.net.SocketPermission": + if (target != null) { + block.socketPerms.add( + new RawSocketPermission( + target, + actions != null ? actions : "connect,resolve", + block.codeBase, + block.source)); + } + break; + case "java.lang.RuntimePermission": + if ("exitVM".equals(target) || (target != null && target.startsWith("exitVM."))) { + block.runtimePerms.add( + new RawRuntimePermission("exitVM", block.codeBase, block.source)); + } else if ("exec".equals(target)) { + block.runtimePerms.add(new RawRuntimePermission("exec", block.codeBase, block.source)); + } + break; + default: + // Unrecognised permission types are ignored (e.g. PropertyPermission in legacy policy) + break; + } + } + } + + /** Converts raw parsed grant blocks into an immutable {@link SolrSecurityPolicy}. */ + private SolrSecurityPolicy buildPolicy(List grants) { + List paths = new ArrayList<>(); + List endpoints = new ArrayList<>(); + List exitCallers = new ArrayList<>(); + List execCallers = new ArrayList<>(); + + for (GrantBlock block : grants) { + for (RawFilePermission fp : block.filePaths) { + boolean recursive = fp.target.endsWith("/-") || fp.target.endsWith("\\-"); + String basePath = recursive ? fp.target.substring(0, fp.target.length() - 2) : fp.target; + paths.add(new PermittedPath(basePath, fp.actions, recursive, fp.source)); + } + for (RawSocketPermission sp : block.socketPerms) { + endpoints.add(new PermittedEndpoint(sp.hostPort, sp.actions, sp.codeBase, sp.source)); + } + for (RawRuntimePermission rp : block.runtimePerms) { + if ("exitVM".equals(rp.type)) { + // codeBase-scoped exitVM grants map to approved exit callers + String pattern = rp.codeBase != null ? rp.codeBase : "*"; + exitCallers.add( + new ApprovedCallSite(pattern, ApprovedCallSite.Operation.EXIT, rp.source)); + } else if ("exec".equals(rp.type)) { + String pattern = rp.codeBase != null ? rp.codeBase : "*"; + execCallers.add( + new ApprovedCallSite(pattern, ApprovedCallSite.Operation.EXEC, rp.source)); + } + } + } + + // Read enforcement mode from system property (not from EnvUtils — agent has no dep on + // solr:core) + String modeStr = System.getProperty("solr.security.agent.mode", "warn"); + SolrSecurityPolicy.EnforcementMode mode = + "enforce".equalsIgnoreCase(modeStr.trim()) + ? SolrSecurityPolicy.EnforcementMode.ENFORCE + : SolrSecurityPolicy.EnforcementMode.WARN; + + return new SolrSecurityPolicy(paths, endpoints, exitCallers, execCallers, mode); + } + + // --------------------------------------------------------------------------- + // Internal data transfer objects for parsed policy entries + // --------------------------------------------------------------------------- + + /** Holds the parsed contents of one grant { } block. */ + static class GrantBlock { + final String codeBase; // null for global grants + final PolicySource source; + final List filePaths = new ArrayList<>(); + final List socketPerms = new ArrayList<>(); + final List runtimePerms = new ArrayList<>(); + + GrantBlock(String codeBase, PolicySource source) { + this.codeBase = codeBase; + this.source = source; + } + } + + static class RawFilePermission { + final String target; + final String actions; + final PolicySource source; + + RawFilePermission(String target, String actions, PolicySource source) { + this.target = target; + this.actions = actions; + this.source = source; + } + } + + static class RawSocketPermission { + final String hostPort; + final String actions; + final String codeBase; + final PolicySource source; + + RawSocketPermission(String hostPort, String actions, String codeBase, PolicySource source) { + this.hostPort = hostPort; + this.actions = actions; + this.codeBase = codeBase; + this.source = source; + } + } + + static class RawRuntimePermission { + final String type; // "exitVM" or "exec" + final String codeBase; + final PolicySource source; + + RawRuntimePermission(String type, String codeBase, PolicySource source) { + this.type = type; + this.codeBase = codeBase; + this.source = source; + } + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java new file mode 100644 index 000000000000..6ce93194ab5e --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import net.bytebuddy.asm.Advice; + +/** + * ByteBuddy {@link Advice} interceptor for child process spawning. + * + *

Intercepts {@code ProcessBuilder.start()} and {@code Runtime.exec()} to enforce the {@link + * SolrSecurityPolicy#approvedExecCallers()} list. By default, no call sites are approved in the + * production policy (the list is empty), so all process-spawning attempts will be flagged unless an + * operator explicitly adds an entry to {@code agent-security-extra.policy}. + * + *

This interceptor is not present in the OpenSearch {@code agent-sm} module; it is a Solr + * addition to cover the {@code ProcessBuilder} usage sites in Solr core (FR-007). + * + *

Known legitimate process-spawning call sites in Solr (kept out of the default production + * policy because they use {@code ProcessHandle}, not {@code ProcessBuilder}): + * + *

    + *
  • {@code org.apache.solr.cli.SolrProcessManager} — JVM discovery + *
+ */ +public final class ProcessExecInterceptor { + + private ProcessExecInterceptor() {} + + /** Called before {@code ProcessBuilder.start()}. */ + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onProcessBuilderStart() { + checkExec("ProcessBuilder.start()"); + } + + /** + * Called before {@code Runtime.exec(String[])}. + * + * @param command the command array (first element used for the violation log target) + */ + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onRuntimeExec(@Advice.Argument(0) String[] command) { + String target = + command != null && command.length > 0 + ? "Runtime.exec(" + command[0] + ")" + : "Runtime.exec()"; + checkExec(target); + } + + // --------------------------------------------------------------------------- + // Core check logic + // --------------------------------------------------------------------------- + + /** + * Checks whether the current top caller is in the approved exec-caller list. Delegates to {@link + * SecurityViolationLogger} on violation. + * + * @param target a human-readable description of the intercepted call for the violation log + */ + static void checkExec(String target) { + if (!SolrSecurityPolicy.isInitialized()) return; + + SolrSecurityPolicy policy = SolrSecurityPolicy.getInstance(); + String caller = StackInspector.topCallerClassName(); + + if (!policy.isExecApproved(caller)) { + ViolationMetricsReporter.incrementExec(); + SecurityViolationLogger.log( + SecurityViolationLogger.ViolationType.PROCESS_EXEC, + target, + caller, + policy.enforcementMode()); + if (policy.enforcementMode() == SolrSecurityPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException( + "Process spawning denied by Solr security agent — unapproved caller: " + caller); + } + } + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java new file mode 100644 index 000000000000..8dee74b8d3ea --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.lang.invoke.MethodHandles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Emits structured log entries for security policy violations detected by the Solr security agent. + * + *

Log format

+ * + *
{@code
+ * SECURITY VIOLATION [TYPE] target= caller= mode= source=
+ * }
+ * + *

In {@link SolrSecurityPolicy.EnforcementMode#WARN warn mode} entries are logged at {@code + * WARN} level and the operation is allowed to proceed. In {@link + * SolrSecurityPolicy.EnforcementMode#ENFORCE enforce mode} entries are logged at {@code ERROR} + * level and the operation must be blocked by the calling interceptor. + * + *

The {@code source} field identifies whether the matching policy entry (if any) came from the + * default bundled policy ({@code DEFAULT}) or from an operator extension ({@code OPERATOR}). For + * violations where no entry matched at all, {@code source} is omitted. + * + *

SLF4J and classloader boundary

+ * + * Agent classes may be injected into the bootstrap classloader, where SLF4J is not directly + * accessible. The logger is obtained lazily via the context classloader to bridge this boundary. If + * SLF4J is not yet available (early startup), violations are written to {@code System.err}. + */ +public final class SecurityViolationLogger { + + /** The operation types that can trigger a violation. */ + public enum ViolationType { + FILE_READ, + FILE_WRITE, + FILE_DELETE, + NETWORK_CONNECT, + SYSTEM_EXIT, + PROCESS_EXEC + } + + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private SecurityViolationLogger() {} + + /** + * Records a security violation. + * + * @param type the category of the blocked/warned operation + * @param target the resource that was targeted (path, host:port, or operation descriptor) + * @param caller the fully-qualified name of the top non-JDK caller class + * @param mode the current enforcement mode + * @param source the policy source tag ({@code "DEFAULT"}, {@code "OPERATOR"}, or {@code null} if + * no entry matched) + */ + public static void log( + ViolationType type, + String target, + String caller, + SolrSecurityPolicy.EnforcementMode mode, + String source) { + + String message = buildMessage(type, target, caller, mode, source); + + if (mode == SolrSecurityPolicy.EnforcementMode.ENFORCE) { + log.error(message); + if (log.isDebugEnabled()) { + log.debug( + "Stack trace for violation above:", new RuntimeException("violation stack trace")); + } + } else { + log.warn(message); + if (log.isDebugEnabled()) { + log.debug( + "Stack trace for violation above:", new RuntimeException("violation stack trace")); + } + } + } + + /** + * Convenience overload without a {@code source} field (used during early startup before policy + * source tagging is available). + */ + public static void log( + ViolationType type, String target, String caller, SolrSecurityPolicy.EnforcementMode mode) { + log(type, target, caller, mode, null); + } + + static String buildMessage( + ViolationType type, + String target, + String caller, + SolrSecurityPolicy.EnforcementMode mode, + String source) { + + StringBuilder sb = new StringBuilder(); + sb.append("SECURITY VIOLATION [") + .append(type.name()) + .append("] target=") + .append(target) + .append(" caller=") + .append(caller) + .append(" mode=") + .append(mode.name()); + if (source != null) { + sb.append(" source=").append(source); + } + return sb.toString(); + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java new file mode 100644 index 000000000000..15b52b8bcf57 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java @@ -0,0 +1,202 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.lang.instrument.Instrumentation; +import java.nio.file.Path; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatchers; + +/** + * Java agent entry point for the Solr security agent. + * + *

The JVM invokes {@link #premain(String, Instrumentation)} before the application main method + * when the agent JAR is listed on the command line as {@code -javaagent:solr-agent-sm-*.jar}. The + * {@link #agentmain(String, Instrumentation)} method supports attach-based loading (not used by + * Solr's startup scripts but retained for tooling compatibility). + * + *

Startup sequence

+ * + *
    + *
  1. Locate and parse {@code agent-security.policy} (and the optional {@code + * agent-security-extra.policy}) via {@link PolicyLoader}. + *
  2. Initialize the {@link SolrSecurityPolicy} singleton. + *
  3. Register all four ByteBuddy interceptors with the JVM instrumentation API. + *
  4. If policy loading fails and enforcement mode is {@code ENFORCE}, halt the JVM; in {@code + * WARN} mode, log the error and continue without protection. + *
+ * + *

Bootstrap injection

+ * + * The interceptor classes ({@link FileAccessInterceptor}, etc.) are injected into the bootstrap + * classloader using {@code ClassInjector.UsingUnsafe.ofBootLoader()} so that they can intercept JDK + * methods which are loaded by the bootstrap loader. The {@code @SuppressForbidden} annotation on + * the injection call acknowledges the intentional use of {@code sun.misc.Unsafe}. + */ +public final class SolrAgentEntryPoint { + + private SolrAgentEntryPoint() {} + + /** + * Called by the JVM before the application main class is loaded, when the agent JAR is specified + * via {@code -javaagent:}. + * + * @param agentArgs optional agent argument string (unused) + * @param inst the {@link Instrumentation} instance provided by the JVM + */ + public static void premain(String agentArgs, Instrumentation inst) { + bootOtel(inst); + } + + /** + * Called by the JVM when the agent is attached dynamically (post-startup). Delegates to {@link + * #premain(String, Instrumentation)}. + * + * @param agentArgs optional agent argument string (unused) + * @param inst the {@link Instrumentation} instance provided by the JVM + */ + public static void agentmain(String agentArgs, Instrumentation inst) { + premain(agentArgs, inst); + } + + // --------------------------------------------------------------------------- + // Internal bootstrap logic + // --------------------------------------------------------------------------- + + @SuppressForbidden( + reason = + "System.err is the only output available during premain (before logging is initialized). " + + "System.exit(1) is required to halt the JVM on a fatal policy-load failure in " + + "enforce mode — this is intentional agent behavior, not application code.") + private static void bootOtel(Instrumentation inst) { + // Locate the default policy file next to the agent JAR. + Path defaultPolicyPath = resolveDefaultPolicyPath(); + + SolrSecurityPolicy policy = null; + try { + PolicyLoader loader = new PolicyLoader(); + policy = loader.load(defaultPolicyPath); + } catch (IllegalStateException e) { + // Policy load failed. + String modeStr = System.getProperty("solr.security.agent.mode", "warn"); + if ("enforce".equalsIgnoreCase(modeStr.trim())) { + System.err.println( + "[Solr SecurityAgent] FATAL: Cannot load security policy in enforce mode. " + + "Solr will not start. Error: " + + e.getMessage()); + System.exit(1); + } else { + System.err.println( + "[Solr SecurityAgent] WARNING: Cannot load security policy (" + + e.getMessage() + + "). Security controls are inactive."); + return; + } + } + + SolrSecurityPolicy.initialize(policy); + + // Register ByteBuddy interceptors. + try { + installInterceptors(inst); + } catch (Exception e) { + System.err.println("[Solr SecurityAgent] Failed to install interceptors: " + e); + } + + System.err.println( + "[Solr SecurityAgent] Security agent active — mode=" + + policy.enforcementMode() + + ", permitted paths=" + + policy.permittedPaths().size() + + ", permitted endpoints=" + + policy.permittedEndpoints().size()); + } + + /** + * Installs all four ByteBuddy interceptors using the provided {@link Instrumentation} instance. + * The interceptor classes are injected into the bootstrap classloader so that they can redefine + * JDK methods. + */ + private static void installInterceptors(Instrumentation inst) { + new AgentBuilder.Default() + // Intercept java.nio.file.Files read/copy/move operations → FileAccessInterceptor + .type(ElementMatchers.named("java.nio.file.Files")) + .transform( + (builder, type, classLoader, module, domain) -> + builder.visit( + Advice.to(FileAccessInterceptor.class) + .on( + ElementMatchers.named("newInputStream") + .or(ElementMatchers.named("readAllBytes")) + .or(ElementMatchers.named("readString")) + .or(ElementMatchers.named("readAllLines")) + .or(ElementMatchers.named("newOutputStream")) + .or(ElementMatchers.named("write")) + .or(ElementMatchers.named("delete")) + .or(ElementMatchers.named("deleteIfExists")) + .or(ElementMatchers.named("copy")) + .or(ElementMatchers.named("move"))))) + // Intercept SocketChannel.connect(SocketAddress) → NetworkAccessInterceptor + .type( + ElementMatchers.named("java.nio.channels.SocketChannel") + .or(ElementMatchers.named("java.net.Socket"))) + .transform( + (builder, type, classLoader, module, domain) -> + builder.visit( + Advice.to(NetworkAccessInterceptor.class).on(ElementMatchers.named("connect")))) + // Intercept System.exit(int) → ExitInterceptor.onSystemExit + .type(ElementMatchers.named("java.lang.System")) + .transform( + (builder, type, classLoader, module, domain) -> + builder.visit(Advice.to(ExitInterceptor.class).on(ElementMatchers.named("exit")))) + // Intercept Runtime.halt(int) → ExitInterceptor.onRuntimeHalt + .type(ElementMatchers.named("java.lang.Runtime")) + .transform( + (builder, type, classLoader, module, domain) -> + builder.visit(Advice.to(ExitInterceptor.class).on(ElementMatchers.named("halt")))) + // Intercept ProcessBuilder.start() → ProcessExecInterceptor.onProcessBuilderStart + .type(ElementMatchers.named("java.lang.ProcessBuilder")) + .transform( + (builder, type, classLoader, module, domain) -> + builder.visit( + Advice.to(ProcessExecInterceptor.class).on(ElementMatchers.named("start")))) + // Intercept Runtime.exec(String[]) → ProcessExecInterceptor.onRuntimeExec + .type(ElementMatchers.named("java.lang.Runtime")) + .transform( + (builder, type, classLoader, module, domain) -> + builder.visit( + Advice.to(ProcessExecInterceptor.class).on(ElementMatchers.named("exec")))) + .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) + .with(AgentBuilder.TypeStrategy.Default.REDEFINE) + .installOn(inst); + } + + /** + * Resolves the default policy file path. Looks for {@code server/etc/agent-security.policy} + * relative to the Solr installation root ({@code solr.install.dir}), then falls back to {@code + * jetty.home/../etc/agent-security.policy}. + */ + private static Path resolveDefaultPolicyPath() { + String installDir = System.getProperty("solr.install.dir"); + if (installDir != null && !installDir.isBlank()) { + return Path.of(installDir, "server", "etc", "agent-security.policy"); + } + String jettyHome = System.getProperty("jetty.home", System.getProperty("server.dir", ".")); + return Path.of(jettyHome, "etc", "agent-security.policy"); + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrSecurityPolicy.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrSecurityPolicy.java new file mode 100644 index 000000000000..e313d47a3d5c --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrSecurityPolicy.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * Immutable singleton that holds the active security policy for the Solr JVM. + * + *

The policy is loaded once at JVM startup by {@link PolicyLoader} and must not be modified + * afterwards. Any attempt to replace the singleton after it has been set throws a {@link + * SecurityException}. + * + *

The singleton is stored as a plain {@code static volatile} field so that it is visible to all + * classloaders, including bootstrap-injected agent classes. The enforcement mode is read directly + * from the system property {@code solr.security.agent.mode} (set by the startup script from the + * environment variable {@code SOLR_SECURITY_AGENT_MODE}). {@code EnvUtils} from {@code solr:core} + * is intentionally not used here because the agent JAR has no compile-time dependency on Solr + * application code. + */ +public final class SolrSecurityPolicy { + + /** Whether violations block the operation or are merely logged. */ + public enum EnforcementMode { + /** Violations are logged at WARN level; the operation is allowed to proceed. */ + WARN, + /** Violations are logged at ERROR level and blocked with a {@link SecurityException}. */ + ENFORCE + } + + // Singleton holder — set once at premain; never null after initialization. + private static volatile SolrSecurityPolicy instance; + + private final List permittedPaths; + private final List permittedEndpoints; + private final List approvedExitCallers; + private final List approvedExecCallers; + private final EnforcementMode enforcementMode; + private final Set trustedFileSystems; + + /** Constructs the policy. Called exclusively by {@link PolicyLoader#load(java.nio.file.Path)}. */ + SolrSecurityPolicy( + List permittedPaths, + List permittedEndpoints, + List approvedExitCallers, + List approvedExecCallers, + EnforcementMode enforcementMode) { + this( + permittedPaths, + permittedEndpoints, + approvedExitCallers, + approvedExecCallers, + enforcementMode, + Set.of()); + } + + /** + * Constructs the policy with an explicit set of trusted filesystem schemes (e.g. in-memory FS + * schemes used in tests). + */ + SolrSecurityPolicy( + List permittedPaths, + List permittedEndpoints, + List approvedExitCallers, + List approvedExecCallers, + EnforcementMode enforcementMode, + Set trustedFileSystems) { + this.permittedPaths = Collections.unmodifiableList(permittedPaths); + this.permittedEndpoints = Collections.unmodifiableList(permittedEndpoints); + this.approvedExitCallers = Collections.unmodifiableList(approvedExitCallers); + this.approvedExecCallers = Collections.unmodifiableList(approvedExecCallers); + this.enforcementMode = enforcementMode; + this.trustedFileSystems = Collections.unmodifiableSet(trustedFileSystems); + } + + // --------------------------------------------------------------------------- + // Singleton management + // --------------------------------------------------------------------------- + + /** + * Sets the global singleton policy. May only be called once; subsequent calls throw {@link + * SecurityException}. + */ + public static void initialize(SolrSecurityPolicy policy) { + synchronized (SolrSecurityPolicy.class) { + if (instance != null) { + throw new SecurityException( + "SolrSecurityPolicy has already been initialized and cannot be replaced. " + + "This is a programming error; only SolrAgentEntryPoint.premain() should call initialize()."); + } + instance = policy; + } + } + + /** + * Returns the active global policy. + * + * @throws IllegalStateException if the policy has not yet been initialized + */ + public static SolrSecurityPolicy getInstance() { + SolrSecurityPolicy p = instance; + if (p == null) { + throw new IllegalStateException( + "SolrSecurityPolicy has not been initialized. " + + "Ensure the Solr security agent JAR is on the -javaagent: command-line."); + } + return p; + } + + /** + * Returns {@code true} if the singleton has already been initialized. Used by the agent entry + * point to detect double-loading. + */ + public static boolean isInitialized() { + return instance != null; + } + + /** + * Resets the singleton to {@code null} so that tests can re-initialize it between test methods. + * + *

This method is package-private and intended exclusively for unit tests in the {@code + * org.apache.solr.security.agent} package. Production code must never call this method. + */ + static void resetForTesting() { + instance = null; + } + + // --------------------------------------------------------------------------- + // Policy accessors + // --------------------------------------------------------------------------- + + /** Permitted file-system paths derived from both the default policy and operator extensions. */ + public List permittedPaths() { + return permittedPaths; + } + + /** Permitted outbound network endpoints. */ + public List permittedEndpoints() { + return permittedEndpoints; + } + + /** Classes approved to call {@code System.exit()} or {@code Runtime.halt()}. */ + public List approvedExitCallers() { + return approvedExitCallers; + } + + /** + * Classes approved to spawn child processes via {@code ProcessBuilder} or {@code Runtime.exec()}. + */ + public List approvedExecCallers() { + return approvedExecCallers; + } + + /** + * Current enforcement mode. {@link EnforcementMode#WARN} allows violations; {@link + * EnforcementMode#ENFORCE} blocks them. + */ + public EnforcementMode enforcementMode() { + return enforcementMode; + } + + /** + * Filesystem scheme names that are exempt from path-based checks (e.g. in-memory filesystems used + * in tests). + */ + public Set trustedFileSystems() { + return trustedFileSystems; + } + + // --------------------------------------------------------------------------- + // Policy checks (convenience helpers called by interceptors) + // --------------------------------------------------------------------------- + + /** + * Returns {@code true} if at least one {@link PermittedPath} in this policy permits the given + * action on the given resolved (real) path. + */ + public boolean isPathPermitted(String resolvedPath, String action) { + for (PermittedPath p : permittedPaths) { + if (p.permits(resolvedPath, action)) return true; + } + return false; + } + + /** + * Returns {@code true} if at least one {@link ApprovedCallSite} with {@link + * ApprovedCallSite.Operation#EXIT} matches the given class name. + */ + public boolean isExitApproved(String className) { + for (ApprovedCallSite cs : approvedExitCallers) { + if (cs.operation() == ApprovedCallSite.Operation.EXIT && cs.matches(className)) return true; + } + return false; + } + + /** + * Returns {@code true} if at least one {@link ApprovedCallSite} with {@link + * ApprovedCallSite.Operation#EXEC} matches the given class name. + */ + public boolean isExecApproved(String className) { + for (ApprovedCallSite cs : approvedExecCallers) { + if (cs.operation() == ApprovedCallSite.Operation.EXEC && cs.matches(className)) return true; + } + return false; + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/StackInspector.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/StackInspector.java new file mode 100644 index 000000000000..44d227766167 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/StackInspector.java @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Walks the current call stack and returns the ordered list of non-JDK caller classes. + * + *

This class uses {@link StackWalker#getInstance(StackWalker.Option)} with {@code + * StackWalker.Option#RETAIN_CLASS_REFERENCE} to obtain live {@link Class} objects for each frame. + * JDK-internal frames (whose class is loaded from the {@code jrt:/} location) are filtered out. + * + *

Virtual thread compatibility

+ * + * {@code StackWalker} is virtual-thread–safe by specification. Unlike deprecated {@code + * Thread.currentThread()} or {@code ThreadGroup} approaches, this implementation does not assume + * thread identity, making enforcement decisions correct for both platform threads and Project Loom + * virtual threads. + */ +public final class StackInspector { + + /** + * StackWalker with class references retained. May be {@code null} if the security environment + * (e.g. Java SecurityManager in tests) denies the {@code getStackWalkerWithClassReference} + * permission — in that case caller identification returns {@code ""}. + */ + private static final StackWalker WALKER; + + static { + StackWalker w; + try { + w = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); + } catch (SecurityException e) { + // Java SecurityManager denied the RuntimePermission "getStackWalkerWithClassReference". + // This can happen in constrained test environments. Fall back to null; all caller-based + // checks will treat the caller as "" (no class reference available). + w = null; + } + WALKER = w; + } + + /** + * The agent's own interceptor and infrastructure classes. These are always on the call stack when + * enforcement is triggered and must be excluded from caller identification. + */ + private static final Set AGENT_INFRASTRUCTURE = + Set.of( + "org.apache.solr.security.agent.StackInspector", + "org.apache.solr.security.agent.FileAccessInterceptor", + "org.apache.solr.security.agent.NetworkAccessInterceptor", + "org.apache.solr.security.agent.ExitInterceptor", + "org.apache.solr.security.agent.ProcessExecInterceptor", + "org.apache.solr.security.agent.SolrSecurityPolicy", + "org.apache.solr.security.agent.SecurityViolationLogger", + "org.apache.solr.security.agent.ViolationMetricsReporter", + "org.apache.solr.security.agent.PolicyLoader", + "org.apache.solr.security.agent.SolrAgentEntryPoint"); + + private StackInspector() {} + + /** + * Returns an ordered list of non-JDK caller classes, starting from the immediate caller of the + * intercepted method and working up the call chain. JDK classes (those loaded from {@code jrt:/} + * or whose class loader is {@code null} — i.e. the bootstrap loader) and ByteBuddy-generated + * classes are excluded. + * + * @return caller classes in call-chain order (innermost first), never {@code null} + */ + public static List> callerClasses() { + if (WALKER == null) return List.of(); + return WALKER.walk( + frames -> { + List> callers = new ArrayList<>(); + frames.forEach( + frame -> { + Class cls = frame.getDeclaringClass(); + if (!isJdkClass(cls) && !isBytebuddyClass(cls) && !isAgentClass(cls)) { + callers.add(cls); + } + }); + return callers; + }); + } + + /** + * Returns the fully-qualified name of the first non-JDK, non-ByteBuddy class in the call stack, + * or {@code ""} if none is found. + */ + public static String topCallerClassName() { + if (WALKER == null) return ""; + return WALKER.walk( + frames -> + frames + .filter( + f -> + !isJdkClass(f.getDeclaringClass()) + && !isBytebuddyClass(f.getDeclaringClass()) + && !isAgentClass(f.getDeclaringClass())) + .findFirst() + .map(f -> f.getDeclaringClass().getName()) + .orElse("")); + } + + /** + * Returns {@code true} if the class should be considered a JDK internal frame and excluded from + * caller analysis. This covers: + * + *
    + *
  • Classes whose classloader is {@code null} (bootstrap loader — Java platform classes) + *
  • Classes from the {@code java.*}, {@code javax.*}, {@code sun.*}, {@code jdk.*} packages + *
+ */ + static boolean isJdkClass(Class cls) { + if (cls.getClassLoader() == null) return true; + String name = cls.getName(); + return name.startsWith("java.") + || name.startsWith("javax.") + || name.startsWith("sun.") + || name.startsWith("jdk.") + || name.startsWith("com.sun."); + } + + /** + * Returns {@code true} if this class is one of the agent's own interceptor or infrastructure + * classes that are always on the call stack when enforcement is triggered and must not be + * reported as the "caller". This uses an explicit allowlist so that test classes in the same + * package are not inadvertently excluded. + */ + static boolean isAgentClass(Class cls) { + return AGENT_INFRASTRUCTURE.contains(cls.getName()); + } + + /** + * Returns {@code true} if this class is a ByteBuddy-generated instrumentation proxy that should + * not appear in violation call-site analysis. + */ + static boolean isBytebuddyClass(Class cls) { + String name = cls.getName(); + return name.contains("$ByteBuddy$") || name.startsWith("net.bytebuddy."); + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SuppressForbidden.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SuppressForbidden.java new file mode 100644 index 000000000000..555c7be5a9c4 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SuppressForbidden.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Suppresses forbidden-API checker violations for the annotated element. The {@code reason} field + * must explain why the forbidden API is necessary here. + * + *

This local copy mirrors the semantics of {@code org.apache.solr.common.util.SuppressForbidden} + * and {@code org.apache.lucene.util.SuppressForbidden}. The Gradle forbidden-apis plugin matches + * any annotation named {@code SuppressForbidden} regardless of package. + */ +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR}) +public @interface SuppressForbidden { + /** Explanation of why the forbidden API is necessary at this call site. */ + String reason(); +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java new file mode 100644 index 000000000000..6c71a6e27988 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java @@ -0,0 +1,209 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Supplier; + +/** + * Maintains per-type violation counters and registers them with Solr's metrics registry once it + * becomes available. + * + *

Deferred registration pattern

+ * + * The agent starts (via {@code premain()}) before Solr's {@code SolrMetricManager} is initialized. + * Counters are maintained from the very first violation using {@link LongAdder}s. When {@code + * CoreContainer} initializes {@code SolrMetricManager}, it calls {@link + * #registerWithSolrMetrics(Object, String)} reflectively (to avoid a compile-time dependency on + * {@code solr:agent-sm} from {@code solr:core}). At that point the accumulated counts are already + * in the counters and the registration just wires them to the metrics registry. + * + *

Metric names

+ * + *
    + *
  • {@code security.agent.violations.file} + *
  • {@code security.agent.violations.network} + *
  • {@code security.agent.violations.exit} + *
  • {@code security.agent.violations.exec} + *
+ */ +public final class ViolationMetricsReporter { + + // Per-type counters — incremented atomically from interceptor hot paths. + private static final LongAdder FILE_COUNTER = new LongAdder(); + private static final LongAdder NETWORK_COUNTER = new LongAdder(); + private static final LongAdder EXIT_COUNTER = new LongAdder(); + private static final LongAdder EXEC_COUNTER = new LongAdder(); + + // Metric names exposed in the Solr metrics registry. + public static final String METRIC_FILE = "security.agent.violations.file"; + public static final String METRIC_NETWORK = "security.agent.violations.network"; + public static final String METRIC_EXIT = "security.agent.violations.exit"; + public static final String METRIC_EXEC = "security.agent.violations.exec"; + + private ViolationMetricsReporter() {} + + // --------------------------------------------------------------------------- + // Counter increment API (called by interceptors) + // --------------------------------------------------------------------------- + + /** Increments the file-access violation counter. */ + public static void incrementFile() { + FILE_COUNTER.increment(); + } + + /** Increments the network-connection violation counter. */ + public static void incrementNetwork() { + NETWORK_COUNTER.increment(); + } + + /** Increments the System.exit() violation counter. */ + public static void incrementExit() { + EXIT_COUNTER.increment(); + } + + /** Increments the process-exec violation counter. */ + public static void incrementExec() { + EXEC_COUNTER.increment(); + } + + // --------------------------------------------------------------------------- + // Counter read API (used by tests) + // --------------------------------------------------------------------------- + + /** Returns the current file-access violation count. */ + public static long fileCount() { + return FILE_COUNTER.sum(); + } + + /** Returns the current network-connection violation count. */ + public static long networkCount() { + return NETWORK_COUNTER.sum(); + } + + /** Returns the current System.exit() violation count. */ + public static long exitCount() { + return EXIT_COUNTER.sum(); + } + + /** Returns the current process-exec violation count. */ + public static long execCount() { + return EXEC_COUNTER.sum(); + } + + // --------------------------------------------------------------------------- + // Deferred metrics registration (called reflectively from CoreContainer) + // --------------------------------------------------------------------------- + + /** + * Registers the four violation counters with the given {@code SolrMetricManager} in the specified + * registry. + * + *

This method is called reflectively from {@code CoreContainer} to avoid a compile-time + * dependency between {@code solr:core} and {@code solr:agent-sm}. The signature must match what + * CoreContainer expects: + * + *

{@code
+   * Class.forName("org.apache.solr.security.agent.ViolationMetricsReporter", false, null)
+   *      .getMethod("registerWithSolrMetrics", SolrMetricManager.class, String.class)
+   *      .invoke(null, metricManager, "solr.jvm");
+   * }
+ * + *

Because this module has no compile-time dependency on {@code solr:core}, the parameter type + * is declared as {@link Object}; the reflective call site in {@code CoreContainer} passes the + * real {@code SolrMetricManager} instance. + * + * @param metricManager the {@code SolrMetricManager} instance (type-erased to {@link Object} to + * avoid a compile-time dependency on solr:core) + * @param registryName the target metrics registry name (e.g. {@code "solr.jvm"}) + */ + public static void registerWithSolrMetrics(Object metricManager, String registryName) { + // Reflectively call SolrMetricManager.registerGauge(...) for each counter. + // We use a Supplier so the gauge always returns the current counter value. + try { + Class mmClass = metricManager.getClass(); + // SolrMetricManager.registerGauge(SolrInfoBean reporter, String registry, + // Gauge gauge, String scope, boolean force, String... path) + // We use the simpler overload that accepts a Supplier directly where available. + // Fall back to the Gauge overload if the Supplier overload is absent. + registerGauge(mmClass, metricManager, registryName, METRIC_FILE, FILE_COUNTER::sum); + registerGauge(mmClass, metricManager, registryName, METRIC_NETWORK, NETWORK_COUNTER::sum); + registerGauge(mmClass, metricManager, registryName, METRIC_EXIT, EXIT_COUNTER::sum); + registerGauge(mmClass, metricManager, registryName, METRIC_EXEC, EXEC_COUNTER::sum); + } catch (Exception e) { + // Log to stderr — SLF4J may not be reachable from bootstrap context during premain. + agentErr("[Solr SecurityAgent] Failed to register violation metrics: " + e); + } + } + + private static void registerGauge( + Class mmClass, Object mm, String registry, String metricName, Supplier valueSupplier) + throws Exception { + // Look for registerGauge(SolrInfoBean, String, Gauge, boolean, String, String...) + // The gauge is a lambda; metrics names are split as scope + path segments. + // We use the most compatible call: registerGauge(null, registry, gauge, false, metricName) + Method registerMethod = null; + for (Method m : mmClass.getMethods()) { + if ("registerGauge".equals(m.getName())) { + registerMethod = m; + break; + } + } + if (registerMethod == null) { + agentErr("[Solr SecurityAgent] SolrMetricManager.registerGauge not found"); + return; + } + // Build a com.codahale.metrics.Gauge lambda via a proxy or cast. + // Since we can't import codahale types here, create an anonymous class via reflection. + // Most robust: use the Gauge functional interface via dynamic proxy. + Object gauge = buildGauge(valueSupplier); + // Invoke: registerGauge(SolrInfoBean=null, String registry, Gauge gauge, boolean force, String + // name, String... path) + registerMethod.invoke(mm, null, registry, gauge, false, metricName, new String[0]); + } + + @SuppressForbidden( + reason = + "System.err is the only output channel available during premain/agent bootstrap, " + + "before SLF4J is reachable from the bootstrap classloader.") + private static void agentErr(String msg) { + System.err.println(msg); + } + + @SuppressForbidden( + reason = + "Thread.getContextClassLoader() is required here to locate com.codahale.metrics.Gauge " + + "from the application classloader when this agent class lives in the bootstrap loader.") + private static Object buildGauge(Supplier supplier) throws Exception { + // com.codahale.metrics.Gauge is a single-method interface (functional). + // Create a dynamic proxy implementing Gauge. + Class gaugeInterface = + Class.forName( + "com.codahale.metrics.Gauge", true, Thread.currentThread().getContextClassLoader()); + return Proxy.newProxyInstance( + gaugeInterface.getClassLoader(), + new Class[] {gaugeInterface}, + (proxy, method, args) -> { + if ("getValue".equals(method.getName())) return supplier.get(); + if ("equals".equals(method.getName())) return proxy == args[0]; + if ("hashCode".equals(method.getName())) return System.identityHashCode(proxy); + return null; + }); + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java new file mode 100644 index 000000000000..e4a7eec17d22 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Solr Security Agent — Java Security Manager replacement via ByteBuddy instrumentation. + * + *

This package contains the Java agent entry point and runtime enforcement infrastructure that + * replaces the removed Java Security Manager API. The agent intercepts file access, outbound + * network connections, {@code System.exit()}, and process spawning at the bytecode level, enforcing + * a policy loaded from JDK-style {@code .policy} files. + * + *

Key classes: + * + *

    + *
  • {@link org.apache.solr.security.agent.SolrAgentEntryPoint} — {@code premain()}/{@code + * agentmain()} entry point; registers all ByteBuddy interceptors. + *
  • {@link org.apache.solr.security.agent.PolicyLoader} — parses {@code .policy} files with + * Solr variable substitution. + *
  • {@link org.apache.solr.security.agent.SolrSecurityPolicy} — immutable singleton holding the + * merged default + operator policy. + *
  • {@link org.apache.solr.security.agent.StackInspector} — virtual-thread-safe call chain + * analysis via {@code StackWalker}. + *
  • {@link org.apache.solr.security.agent.SecurityViolationLogger} — structured SLF4J violation + * log emitter. + *
  • {@link org.apache.solr.security.agent.ViolationMetricsReporter} — per-type violation + * counters with deferred registration in {@code SolrMetricManager}. + *
+ */ +package org.apache.solr.security.agent; diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/ExitInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/ExitInterceptorTest.java new file mode 100644 index 000000000000..69f805eafbc9 --- /dev/null +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/ExitInterceptorTest.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.util.List; +import org.apache.solr.SolrTestCase; +import org.junit.Before; +import org.junit.Test; + +/** Unit tests for {@link ExitInterceptor} enforcement logic. */ +public class ExitInterceptorTest extends SolrTestCase { + + private long exitCountBefore; + + @Before + public void snapshotCounters() { + exitCountBefore = ViolationMetricsReporter.exitCount(); + } + + private void initPolicy( + boolean approved, String callerClass, SolrSecurityPolicy.EnforcementMode mode) { + // Reset singleton if already set + resetPolicySingleton(); + List exitCallers = + approved + ? List.of( + new ApprovedCallSite( + callerClass, + ApprovedCallSite.Operation.EXIT, + PolicyLoader.PolicySource.DEFAULT)) + : List.of(); + SolrSecurityPolicy policy = + new SolrSecurityPolicy(List.of(), List.of(), exitCallers, List.of(), mode); + SolrSecurityPolicy.initialize(policy); + } + + /** Resets the static singleton so each test starts fresh. */ + private static void resetPolicySingleton() { + SolrSecurityPolicy.resetForTesting(); + } + + @Test + public void testApprovedCallerDoesNotIncreaseCounter() { + // Use the test class itself as an approved caller + initPolicy( + true, ExitInterceptorTest.class.getName(), SolrSecurityPolicy.EnforcementMode.ENFORCE); + // Directly exercise checkExit with the approved class on the call stack + // We call checkExit with a simulated target — no counter should increment + ExitInterceptor.checkExit("System.exit(0)"); + assertEquals(exitCountBefore, ViolationMetricsReporter.exitCount()); + resetPolicySingleton(); + } + + @Test + public void testUnapprovedCallerInWarnModeIncrementsCounter() { + initPolicy(false, "some.other.Class", SolrSecurityPolicy.EnforcementMode.WARN); + ExitInterceptor.checkExit("System.exit(0)"); + assertEquals(exitCountBefore + 1, ViolationMetricsReporter.exitCount()); + resetPolicySingleton(); + } + + @Test(expected = SecurityException.class) + public void testUnapprovedCallerInEnforceModeThrows() { + initPolicy(false, "some.other.Class", SolrSecurityPolicy.EnforcementMode.ENFORCE); + try { + ExitInterceptor.checkExit("System.exit(0)"); + } finally { + resetPolicySingleton(); + } + } + + @Test + public void testUnapprovedCallerInEnforceModeIncrementsCounter() { + initPolicy(false, "some.other.Class", SolrSecurityPolicy.EnforcementMode.ENFORCE); + long before = ViolationMetricsReporter.exitCount(); + try { + ExitInterceptor.checkExit("System.exit(0)"); + } catch (SecurityException ignored) { + // expected + } + assertEquals(before + 1, ViolationMetricsReporter.exitCount()); + resetPolicySingleton(); + } + + @Test + public void testRuntimeHaltCallsCheckExit() { + initPolicy(false, "nobody", SolrSecurityPolicy.EnforcementMode.WARN); + long before = ViolationMetricsReporter.exitCount(); + ExitInterceptor.checkExit("Runtime.halt(0)"); // same code path + assertEquals(before + 1, ViolationMetricsReporter.exitCount()); + resetPolicySingleton(); + } +} diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/NetworkAccessInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/NetworkAccessInterceptorTest.java new file mode 100644 index 000000000000..b47178985279 --- /dev/null +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/NetworkAccessInterceptorTest.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.List; +import org.apache.solr.SolrTestCase; +import org.junit.Test; + +/** Unit tests for {@link NetworkAccessInterceptor} policy matching logic. */ +public class NetworkAccessInterceptorTest extends SolrTestCase { + + private SolrSecurityPolicy policyWithEndpoint(String hostPort) { + PermittedEndpoint ep = + new PermittedEndpoint(hostPort, "connect,resolve", null, PolicyLoader.PolicySource.DEFAULT); + return new SolrSecurityPolicy( + List.of(), List.of(ep), List.of(), List.of(), SolrSecurityPolicy.EnforcementMode.ENFORCE); + } + + @Test + public void testLoopbackPermittedUnconditionally() throws Exception { + // Even with an empty policy, loopback should be allowed. + SolrSecurityPolicy policy = + new SolrSecurityPolicy( + List.of(), List.of(), List.of(), List.of(), SolrSecurityPolicy.EnforcementMode.ENFORCE); + + InetSocketAddress loopback = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 8983); + // Should not throw — loopback is unconditionally permitted + assertTrue(loopback.getAddress().isLoopbackAddress()); + // checkConnect skips loopback before policy lookup + } + + @Test + public void testExactHostPortPermitted() { + SolrSecurityPolicy policy = policyWithEndpoint("192.168.1.100:8983"); + assertTrue(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.100", 8983)); + assertFalse(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.100", 8984)); + assertFalse(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.101", 8983)); + } + + @Test + public void testWildcardHostPortPermitted() { + // *:8983 should match any host on port 8983 + SolrSecurityPolicy policy = policyWithEndpoint("*:8983"); + assertTrue(NetworkAccessInterceptor.isEndpointPermitted(policy, "10.0.0.5", 8983)); + assertTrue(NetworkAccessInterceptor.isEndpointPermitted(policy, "some-other-host", 8983)); + assertFalse(NetworkAccessInterceptor.isEndpointPermitted(policy, "10.0.0.5", 9983)); + } + + @Test + public void testPortRangePermitted() { + SolrSecurityPolicy policy = policyWithEndpoint("192.168.1.1:8000-9000"); + assertTrue(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.1", 8983)); + assertTrue(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.1", 8000)); + assertTrue(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.1", 9000)); + assertFalse(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.1", 7999)); + assertFalse(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.1", 9001)); + } + + @Test + public void testCodebaseScopedEntrySkipped() { + // codeBase-scoped entries should be ignored in the path-based check + PermittedEndpoint codeBasedEp = + new PermittedEndpoint( + "*", + "connect,resolve", + "file:/opt/solr/modules/jwt-auth/-", + PolicyLoader.PolicySource.DEFAULT); + SolrSecurityPolicy policy = + new SolrSecurityPolicy( + List.of(), + List.of(codeBasedEp), + List.of(), + List.of(), + SolrSecurityPolicy.EnforcementMode.ENFORCE); + // Should return false because the only entry is codeBase-scoped + assertFalse(NetworkAccessInterceptor.isEndpointPermitted(policy, "external.host", 443)); + } + + @Test + public void testUnlistedHostPortBlocked() { + SolrSecurityPolicy policy = policyWithEndpoint("localhost:8983"); + assertFalse(NetworkAccessInterceptor.isEndpointPermitted(policy, "10.0.0.1", 443)); + } + + @Test + public void testBroadWildcardPermitsAll() { + SolrSecurityPolicy policy = policyWithEndpoint("*"); + assertTrue(NetworkAccessInterceptor.isEndpointPermitted(policy, "anything.com", 443)); + } +} diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java new file mode 100644 index 000000000000..0b3a4b842160 --- /dev/null +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.apache.solr.SolrTestCase; +import org.junit.After; +import org.junit.Test; + +/** + * Tests for the operator extension policy file ({@code agent-security-extra.policy}). + * + *

Verifies that: + * + *

    + *
  • An extra policy file present at the configured path is loaded and merged with the default + * policy. + *
  • Entries from the extra policy are tagged {@link PolicyLoader.PolicySource#OPERATOR}. + *
  • Paths listed only in the extra policy are permitted; unlisted paths remain blocked. + *
  • When the extra policy file is absent the default policy still loads normally. + *
  • A malformed extra policy causes {@link IllegalStateException} with a descriptive message. + *
  • The {@code source=OPERATOR} tag is emitted in violation log entries for paths matched by + * operator-policy entries. + *
+ */ +public class PolicyLoaderOperatorExtensionTest extends SolrTestCase { + + @After + public void clearExtraPolicy() { + System.clearProperty("solr.security.agent.extra.policy"); + } + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + private Path writeDefaultPolicy(Path dir) throws Exception { + Path policy = dir.resolve("agent-security.policy"); + Files.writeString( + policy, + "grant {\n" + " permission java.io.FilePermission \"/opt/solr/-\", \"read\";\n" + "};\n", + StandardCharsets.UTF_8); + return policy; + } + + private SolrSecurityPolicy loadWithExtra(Path defaultPolicy, Path extraPolicy) { + if (extraPolicy != null) { + System.setProperty("solr.security.agent.extra.policy", extraPolicy.toString()); + } + return new PolicyLoader().load(defaultPolicy); + } + + // --------------------------------------------------------------------------- + // Extra policy present — entries merged and tagged OPERATOR + // --------------------------------------------------------------------------- + + @Test + public void testExtraPolicyPathIsPermitted() throws Exception { + Path tmpDir = createTempDir(); + Path defaultPolicy = writeDefaultPolicy(tmpDir); + + Path extraPolicy = tmpDir.resolve("agent-security-extra.policy"); + Files.writeString( + extraPolicy, + "grant {\n" + + " permission java.io.FilePermission \"" + + tmpDir + + "/-\", \"read\";\n" + + "};\n", + StandardCharsets.UTF_8); + + SolrSecurityPolicy policy = loadWithExtra(defaultPolicy, extraPolicy); + assertTrue(policy.isPathPermitted(tmpDir.resolve("data.txt").toString(), "read")); + } + + @Test + public void testUnlistedPathStillBlockedWhenExtraPolicyPresent() throws Exception { + Path tmpDir = createTempDir(); + Path defaultPolicy = writeDefaultPolicy(tmpDir); + + Path extraPolicy = tmpDir.resolve("agent-security-extra.policy"); + Files.writeString( + extraPolicy, + "grant {\n" + + " permission java.io.FilePermission \"" + + tmpDir + + "/-\", \"read\";\n" + + "};\n", + StandardCharsets.UTF_8); + + SolrSecurityPolicy policy = loadWithExtra(defaultPolicy, extraPolicy); + // /etc is not in either policy + assertFalse(policy.isPathPermitted("/etc/shadow", "read")); + } + + @Test + public void testExtraPolicyEntriesTaggedOperator() throws Exception { + Path tmpDir = createTempDir(); + Path defaultPolicy = writeDefaultPolicy(tmpDir); + + Path extraPolicy = tmpDir.resolve("agent-security-extra.policy"); + Files.writeString( + extraPolicy, + "grant {\n" + + " permission java.io.FilePermission \"" + + tmpDir + + "/-\", \"read\";\n" + + "};\n", + StandardCharsets.UTF_8); + + SolrSecurityPolicy policy = loadWithExtra(defaultPolicy, extraPolicy); + List paths = policy.permittedPaths(); + boolean hasOperator = + paths.stream().anyMatch(p -> p.source() == PolicyLoader.PolicySource.OPERATOR); + assertTrue("Expected at least one OPERATOR-sourced path entry", hasOperator); + } + + @Test + public void testDefaultPolicyEntriesTaggedDefault() throws Exception { + Path tmpDir = createTempDir(); + Path defaultPolicy = writeDefaultPolicy(tmpDir); + + SolrSecurityPolicy policy = loadWithExtra(defaultPolicy, null); + List paths = policy.permittedPaths(); + boolean hasDefault = + paths.stream().anyMatch(p -> p.source() == PolicyLoader.PolicySource.DEFAULT); + assertTrue("Expected at least one DEFAULT-sourced path entry", hasDefault); + } + + // --------------------------------------------------------------------------- + // Extra policy absent — default still loads + // --------------------------------------------------------------------------- + + @Test + public void testExtraPolicyAbsentIsNonFatal() throws Exception { + Path tmpDir = createTempDir(); + Path defaultPolicy = writeDefaultPolicy(tmpDir); + + // Point to a non-existent extra policy + System.setProperty( + "solr.security.agent.extra.policy", tmpDir.resolve("nonexistent.policy").toString()); + + // Should not throw; default policy still loads + SolrSecurityPolicy policy = new PolicyLoader().load(defaultPolicy); + assertTrue(policy.isPathPermitted("/opt/solr/conf", "read")); + } + + // --------------------------------------------------------------------------- + // Malformed extra policy — lenient parsing, default still enforced + // --------------------------------------------------------------------------- + + @Test + public void testMalformedExtraPolicyIsSkippedGracefully() throws Exception { + Path tmpDir = createTempDir(); + Path defaultPolicy = writeDefaultPolicy(tmpDir); + + Path extraPolicy = tmpDir.resolve("agent-security-extra.policy"); + // Content with no recognizable grant blocks — parser silently produces empty result + Files.writeString(extraPolicy, "THIS IS NOT A VALID POLICY\n", StandardCharsets.UTF_8); + + System.setProperty("solr.security.agent.extra.policy", extraPolicy.toString()); + + // Should not throw; default policy still loads + SolrSecurityPolicy policy = new PolicyLoader().load(defaultPolicy); + // Default policy (/opt/solr) is still active + assertTrue(policy.isPathPermitted("/opt/solr/conf", "read")); + // Malformed extra policy adds no new paths + assertFalse(policy.isPathPermitted(tmpDir.toString(), "read")); + } + + // --------------------------------------------------------------------------- + // Source field in violation log + // --------------------------------------------------------------------------- + + @Test + public void testViolationLogIncludesSourceField() { + // Verify the log message builder includes the source field for OPERATOR entries + String msg = + SecurityViolationLogger.buildMessage( + SecurityViolationLogger.ViolationType.FILE_READ, + "/tmp/secret.txt", + "com.example.Caller", + SolrSecurityPolicy.EnforcementMode.WARN, + "OPERATOR"); + assertTrue("Expected source=OPERATOR in log message", msg.contains("source=OPERATOR")); + } + + @Test + public void testViolationLogOmitsSourceWhenNull() { + String msg = + SecurityViolationLogger.buildMessage( + SecurityViolationLogger.ViolationType.FILE_READ, + "/tmp/secret.txt", + "com.example.Caller", + SolrSecurityPolicy.EnforcementMode.WARN, + null); + assertFalse("Expected no source= field when source is null", msg.contains("source=")); + } +} diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java new file mode 100644 index 000000000000..7ea5765cc700 --- /dev/null +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java @@ -0,0 +1,254 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import org.apache.solr.SolrTestCase; +import org.junit.Test; + +/** Unit tests for {@link PolicyLoader} — policy parsing, variable substitution, and merging. */ +public class PolicyLoaderTest extends SolrTestCase { + + // --------------------------------------------------------------------------- + // Variable substitution + // --------------------------------------------------------------------------- + + @Test + public void testSubstituteKnownSystemProperties() { + System.setProperty("solr.home", "/opt/solr"); + String result = PolicyLoader.substituteVariables("path=${solr.home}"); + assertEquals("path=/opt/solr", result); + } + + @Test + public void testSolrPortSubstitution() { + System.setProperty("solr.port", "8983"); + String result = PolicyLoader.substituteVariables("*:${solr.port}"); + assertEquals("*:8983", result); + } + + @Test + public void testSolrZkPortDefaultsToSolrPortPlusOneThousand() { + System.setProperty("solr.port", "8983"); + System.clearProperty("solr.zk.port"); + String result = PolicyLoader.substituteVariables("*:${solr.zk.port}"); + assertEquals("*:9983", result); + } + + @Test + public void testSolrZkPortExplicitOverride() { + System.setProperty("solr.zk.port", "2181"); + try { + String result = PolicyLoader.substituteVariables("*:${solr.zk.port}"); + assertEquals("*:2181", result); + } finally { + System.clearProperty("solr.zk.port"); + } + } + + @Test + public void testSolrInstallDirSubstitution() { + System.setProperty("solr.install.dir", "/opt/solr"); + String result = PolicyLoader.substituteVariables("file:${solr.install.dir}/modules/jwt-auth/-"); + assertEquals("file:/opt/solr/modules/jwt-auth/-", result); + } + + @Test + public void testUnknownVariableLeftAsIs() { + String result = PolicyLoader.substituteVariables("path=${unknown.var}"); + assertEquals("path=${unknown.var}", result); + } + + // --------------------------------------------------------------------------- + // Policy block parsing + // --------------------------------------------------------------------------- + + @Test + public void testGlobalGrantFilePermissionParsed() { + String policy = + "grant {\n" + " permission java.io.FilePermission \"/solr/home/-\", \"read\";\n" + "};"; + List blocks = new ArrayList<>(); + PolicyLoader.parsePolicyBlocks(policy, PolicyLoader.PolicySource.DEFAULT, blocks); + + assertEquals(1, blocks.size()); + PolicyLoader.GrantBlock block = blocks.get(0); + assertNull(block.codeBase); + assertEquals(1, block.filePaths.size()); + assertEquals("/solr/home/-", block.filePaths.get(0).target); + assertEquals("read", block.filePaths.get(0).actions); + assertEquals(PolicyLoader.PolicySource.DEFAULT, block.filePaths.get(0).source); + } + + @Test + public void testGlobalGrantSocketPermissionParsed() { + String policy = + "grant {\n" + + " permission java.net.SocketPermission \"*:8983\", \"connect,resolve\";\n" + + "};"; + List blocks = new ArrayList<>(); + PolicyLoader.parsePolicyBlocks(policy, PolicyLoader.PolicySource.DEFAULT, blocks); + + assertEquals(1, blocks.size()); + assertEquals(1, blocks.get(0).socketPerms.size()); + assertEquals("*:8983", blocks.get(0).socketPerms.get(0).hostPort); + assertNull(blocks.get(0).socketPerms.get(0).codeBase); + } + + @Test + public void testCodeBaseScopedGrantParsed() { + String policy = + "grant codeBase \"file:/opt/solr/modules/jwt-auth/-\" {\n" + + " permission java.net.SocketPermission \"*\", \"connect,resolve\";\n" + + "};"; + List blocks = new ArrayList<>(); + PolicyLoader.parsePolicyBlocks(policy, PolicyLoader.PolicySource.DEFAULT, blocks); + + assertEquals(1, blocks.size()); + PolicyLoader.GrantBlock block = blocks.get(0); + assertEquals("file:/opt/solr/modules/jwt-auth/-", block.codeBase); + assertEquals(1, block.socketPerms.size()); + assertEquals("*", block.socketPerms.get(0).hostPort); + assertEquals("file:/opt/solr/modules/jwt-auth/-", block.socketPerms.get(0).codeBase); + } + + @Test + public void testLineCommentsStripped() { + String policy = + "grant { // this is a comment\n" + + " // another comment\n" + + " permission java.io.FilePermission \"/tmp/-\", \"read,write,delete\";\n" + + "};"; + List blocks = new ArrayList<>(); + PolicyLoader.parsePolicyBlocks(policy, PolicyLoader.PolicySource.DEFAULT, blocks); + + assertEquals(1, blocks.size()); + assertEquals(1, blocks.get(0).filePaths.size()); + } + + @Test + public void testUnrecognisedPermissionTypeIgnored() { + String policy = + "grant {\n" + + " permission java.util.PropertyPermission \"*\", \"read\";\n" + + " permission java.io.FilePermission \"/tmp/-\", \"read\";\n" + + "};"; + List blocks = new ArrayList<>(); + PolicyLoader.parsePolicyBlocks(policy, PolicyLoader.PolicySource.DEFAULT, blocks); + + assertEquals(1, blocks.size()); + assertEquals(1, blocks.get(0).filePaths.size()); + // PropertyPermission is silently ignored + } + + // --------------------------------------------------------------------------- + // Full load — file I/O + // --------------------------------------------------------------------------- + + @Test + public void testValidPolicyParsesCorrectly() throws Exception { + Path policyFile = createTempFile("agent-security", ".policy"); + Files.writeString( + policyFile, + "grant {\n" + + " permission java.io.FilePermission \"/solr/home/-\", \"read\";\n" + + " permission java.net.SocketPermission \"localhost:8983\", \"connect,resolve\";\n" + + "};\n", + StandardCharsets.UTF_8); + + PolicyLoader loader = new PolicyLoader(); + SolrSecurityPolicy policy = loader.load(policyFile); + + assertFalse(policy.permittedPaths().isEmpty()); + assertFalse(policy.permittedEndpoints().isEmpty()); + } + + @Test(expected = IllegalStateException.class) + public void testMissingDefaultPolicyThrowsOnLoad() throws Exception { + Path missing = createTempDir().resolve("no-such-policy.policy"); + new PolicyLoader().load(missing); + } + + @Test + public void testExtraPolicyAbsentIsNonFatal() throws Exception { + Path defaultPolicy = createTempFile("agent-security", ".policy"); + Files.writeString( + defaultPolicy, + "grant { permission java.io.FilePermission \"/solr/home/-\", \"read\"; };\n", + StandardCharsets.UTF_8); + + // Point extra-policy at a non-existent file + System.setProperty( + "solr.security.agent.extra.policy", createTempDir().resolve("absent.policy").toString()); + try { + SolrSecurityPolicy policy = new PolicyLoader().load(defaultPolicy); + // Must succeed — absent extra policy is not an error + assertNotNull(policy); + } finally { + System.clearProperty("solr.security.agent.extra.policy"); + } + } + + @Test + public void testExtraPolicyMergedAndTaggedOperator() throws Exception { + Path defaultPolicy = createTempFile("default", ".policy"); + Files.writeString( + defaultPolicy, + "grant { permission java.io.FilePermission \"/solr/home/-\", \"read\"; };\n", + StandardCharsets.UTF_8); + + Path extraPolicy = createTempFile("extra", ".policy"); + Files.writeString( + extraPolicy, + "grant { permission java.io.FilePermission \"/mnt/nfs/-\", \"read\"; };\n", + StandardCharsets.UTF_8); + + System.setProperty("solr.security.agent.extra.policy", extraPolicy.toString()); + try { + SolrSecurityPolicy policy = new PolicyLoader().load(defaultPolicy); + + // Both paths must be present + List paths = policy.permittedPaths(); + assertTrue(paths.stream().anyMatch(p -> p.path().equals("/solr/home"))); + assertTrue(paths.stream().anyMatch(p -> p.path().equals("/mnt/nfs"))); + + // Operator path carries OPERATOR source tag + PermittedPath operatorPath = + paths.stream().filter(p -> p.path().equals("/mnt/nfs")).findFirst().orElseThrow(); + assertEquals(PolicyLoader.PolicySource.OPERATOR, operatorPath.source()); + } finally { + System.clearProperty("solr.security.agent.extra.policy"); + } + } + + @Test + public void testRecursivePathFlagSet() throws Exception { + Path policyFile = createTempFile("agent-security", ".policy"); + Files.writeString( + policyFile, + "grant { permission java.io.FilePermission \"/data/-\", \"read,write,delete\"; };\n", + StandardCharsets.UTF_8); + + SolrSecurityPolicy policy = new PolicyLoader().load(policyFile); + PermittedPath path = policy.permittedPaths().get(0); + assertEquals("/data", path.path()); + assertTrue(path.recursive()); + } +} diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java new file mode 100644 index 000000000000..e2f0d8cc9e88 --- /dev/null +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.util.List; +import org.apache.solr.SolrTestCase; +import org.junit.Before; +import org.junit.Test; + +/** Unit tests for {@link ProcessExecInterceptor} enforcement logic. */ +public class ProcessExecInterceptorTest extends SolrTestCase { + + private long execCountBefore; + + @Before + public void snapshotCounters() { + execCountBefore = ViolationMetricsReporter.execCount(); + } + + private void initPolicy( + boolean approved, String callerClass, SolrSecurityPolicy.EnforcementMode mode) { + resetPolicySingleton(); + List execCallers = + approved + ? List.of( + new ApprovedCallSite( + callerClass, + ApprovedCallSite.Operation.EXEC, + PolicyLoader.PolicySource.DEFAULT)) + : List.of(); + SolrSecurityPolicy policy = + new SolrSecurityPolicy(List.of(), List.of(), List.of(), execCallers, mode); + SolrSecurityPolicy.initialize(policy); + } + + private static void resetPolicySingleton() { + SolrSecurityPolicy.resetForTesting(); + } + + @Test + public void testApprovedCallerDoesNotIncreaseCounter() { + initPolicy( + true, + ProcessExecInterceptorTest.class.getName(), + SolrSecurityPolicy.EnforcementMode.ENFORCE); + ProcessExecInterceptor.checkExec("ProcessBuilder.start()"); + assertEquals(execCountBefore, ViolationMetricsReporter.execCount()); + resetPolicySingleton(); + } + + @Test + public void testUnapprovedCallerInWarnModeIncrementsCounter() { + initPolicy(false, "nobody", SolrSecurityPolicy.EnforcementMode.WARN); + ProcessExecInterceptor.checkExec("ProcessBuilder.start()"); + assertEquals(execCountBefore + 1, ViolationMetricsReporter.execCount()); + resetPolicySingleton(); + } + + @Test(expected = SecurityException.class) + public void testUnapprovedCallerInEnforceModeThrows() { + initPolicy(false, "nobody", SolrSecurityPolicy.EnforcementMode.ENFORCE); + try { + ProcessExecInterceptor.checkExec("ProcessBuilder.start()"); + } finally { + resetPolicySingleton(); + } + } + + @Test + public void testRuntimeExecBlocked() { + initPolicy(false, "nobody", SolrSecurityPolicy.EnforcementMode.WARN); + long before = ViolationMetricsReporter.execCount(); + ProcessExecInterceptor.checkExec("Runtime.exec(ls)"); + assertEquals(before + 1, ViolationMetricsReporter.execCount()); + resetPolicySingleton(); + } + + @Test + public void testWildcardApprovalMatchesAny() { + initPolicy(true, "*", SolrSecurityPolicy.EnforcementMode.ENFORCE); + // Should not throw even for an unknown caller + ProcessExecInterceptor.checkExec("ProcessBuilder.start()"); + assertEquals(execCountBefore, ViolationMetricsReporter.execCount()); + resetPolicySingleton(); + } +} diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java new file mode 100644 index 000000000000..fd069432cbaa --- /dev/null +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.nio.file.Path; +import java.util.List; +import org.apache.solr.SolrTestCase; +import org.junit.After; +import org.junit.Test; + +/** + * Integration-level tests for the Solr security agent in enforce mode. + * + *

These tests exercise the policy engine and interceptor check logic end-to-end without + * requiring the ByteBuddy instrumentation to be active (which requires premain). They validate that + * permitted operations pass, denied operations throw {@link SecurityException}, and that the {@link + * ViolationMetricsReporter} counters increment correctly on each violation. + */ +public class SolrAgentIntegrationTest extends SolrTestCase { + + @After + public void resetSingleton() { + SolrSecurityPolicy.resetForTesting(); + } + + private SolrSecurityPolicy buildEnforcePolicy( + List paths, + List endpoints, + List exitCallers, + List execCallers) { + SolrSecurityPolicy p = + new SolrSecurityPolicy( + paths, endpoints, exitCallers, execCallers, SolrSecurityPolicy.EnforcementMode.ENFORCE); + SolrSecurityPolicy.initialize(p); + return p; + } + + // --------------------------------------------------------------------------- + // File access tests + // --------------------------------------------------------------------------- + + @Test + public void testPermittedFileReadSucceeds() { + Path tmpDir = createTempDir(); + PermittedPath allowed = + new PermittedPath(tmpDir.toString(), "read", true, PolicyLoader.PolicySource.DEFAULT); + buildEnforcePolicy(List.of(allowed), List.of(), List.of(), List.of()); + + Path target = tmpDir.resolve("test.txt"); + // checkPath should not throw for a path inside the permitted dir + FileAccessInterceptor.checkPath( + target, "read", SecurityViolationLogger.ViolationType.FILE_READ); + } + + @Test(expected = SecurityException.class) + public void testDeniedFileReadThrows() { + Path tmpDir = createTempDir(); + // Policy permits nothing + buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); + FileAccessInterceptor.checkPath( + tmpDir.resolve("secret.txt"), "read", SecurityViolationLogger.ViolationType.FILE_READ); + } + + @Test + public void testDeniedFileReadIncrementsFileCounter() { + long before = ViolationMetricsReporter.fileCount(); + buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); + try { + FileAccessInterceptor.checkPath( + Path.of("/etc/passwd"), "read", SecurityViolationLogger.ViolationType.FILE_READ); + } catch (SecurityException ignored) { + // expected + } + assertEquals(before + 1, ViolationMetricsReporter.fileCount()); + } + + // --------------------------------------------------------------------------- + // UNC path tests + // --------------------------------------------------------------------------- + + @Test + public void testUncPathAlwaysBlocked() { + PermittedPath all = new PermittedPath("/", "read", true, PolicyLoader.PolicySource.DEFAULT); + buildEnforcePolicy(List.of(all), List.of(), List.of(), List.of()); + // Even with a broad policy, UNC paths are blocked unconditionally + assertTrue(FileAccessInterceptor.isUncPath("\\\\server\\share\\file")); + assertTrue(FileAccessInterceptor.isUncPath("//server/share/file")); + assertFalse(FileAccessInterceptor.isUncPath("/normal/path")); + } + + // --------------------------------------------------------------------------- + // Network tests + // --------------------------------------------------------------------------- + + @Test + public void testPermittedEndpointNotBlocked() { + PermittedEndpoint ep = + new PermittedEndpoint("*:8983", "connect,resolve", null, PolicyLoader.PolicySource.DEFAULT); + buildEnforcePolicy(List.of(), List.of(ep), List.of(), List.of()); + assertTrue( + NetworkAccessInterceptor.isEndpointPermitted( + SolrSecurityPolicy.getInstance(), "10.0.0.1", 8983)); + } + + @Test(expected = SecurityException.class) + public void testDeniedNetworkConnectThrowsInEnforceMode() throws Exception { + buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); + java.net.InetSocketAddress addr = + new java.net.InetSocketAddress(java.net.InetAddress.getByName("10.0.0.99"), 9999); + NetworkAccessInterceptor.checkConnect(addr); + } + + @Test + public void testDeniedNetworkIncrementsCounter() throws Exception { + long before = ViolationMetricsReporter.networkCount(); + buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); + java.net.InetSocketAddress addr = + new java.net.InetSocketAddress(java.net.InetAddress.getByName("10.0.0.99"), 9999); + try { + NetworkAccessInterceptor.checkConnect(addr); + } catch (SecurityException ignored) { + // expected + } + assertEquals(before + 1, ViolationMetricsReporter.networkCount()); + } + + // --------------------------------------------------------------------------- + // System.exit tests + // --------------------------------------------------------------------------- + + @Test(expected = SecurityException.class) + public void testUnapprovedExitThrowsInEnforceMode() { + buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); + ExitInterceptor.checkExit("System.exit(0)"); + } + + @Test + public void testUnapprovedExitIncrementsCounter() { + long before = ViolationMetricsReporter.exitCount(); + buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); + try { + ExitInterceptor.checkExit("System.exit(0)"); + } catch (SecurityException ignored) { + // expected + } + assertEquals(before + 1, ViolationMetricsReporter.exitCount()); + } + + // --------------------------------------------------------------------------- + // ProcessBuilder tests + // --------------------------------------------------------------------------- + + @Test(expected = SecurityException.class) + public void testUnapprovedProcessExecThrowsInEnforceMode() { + buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); + ProcessExecInterceptor.checkExec("ProcessBuilder.start()"); + } + + @Test + public void testUnapprovedProcessExecIncrementsCounter() { + long before = ViolationMetricsReporter.execCount(); + buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); + try { + ProcessExecInterceptor.checkExec("ProcessBuilder.start()"); + } catch (SecurityException ignored) { + // expected + } + assertEquals(before + 1, ViolationMetricsReporter.execCount()); + } +} diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java new file mode 100644 index 000000000000..17c8ebdd5f69 --- /dev/null +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.apache.solr.SolrTestCase; +import org.junit.After; +import org.junit.Assume; +import org.junit.Test; + +/** + * Tests that symlink-escape attacks are blocked by {@link FileAccessInterceptor}. + * + *

A symlink inside a permitted directory that points to a target outside permitted directories + * must be denied even though the symlink path itself would have matched a permitted rule. The + * interceptor resolves the real path via {@code Path.toRealPath()} before the policy check. + * + *

Tests that require symlink creation are skipped on filesystems that do not support symbolic + * links (e.g. certain Windows configurations). + */ +public class SymlinkEscapeTest extends SolrTestCase { + + @After + public void resetSingleton() { + SolrSecurityPolicy.resetForTesting(); + } + + private void resetSingletonSilent() { + SolrSecurityPolicy.resetForTesting(); + } + + private SolrSecurityPolicy buildPolicyPermitting(Path dir) { + resetSingletonSilent(); + // Use the real (symlink-resolved) path so the policy matches after toRealPath() resolution + String realDirStr; + try { + realDirStr = dir.toRealPath().toString(); + } catch (java.io.IOException e) { + realDirStr = dir.toAbsolutePath().toString(); + } + PermittedPath allowed = + new PermittedPath(realDirStr, "read", true, PolicyLoader.PolicySource.DEFAULT); + SolrSecurityPolicy policy = + new SolrSecurityPolicy( + List.of(allowed), + List.of(), + List.of(), + List.of(), + SolrSecurityPolicy.EnforcementMode.ENFORCE); + SolrSecurityPolicy.initialize(policy); + return policy; + } + + // --------------------------------------------------------------------------- + // Symlink pointing inside the permitted directory — should be allowed + // --------------------------------------------------------------------------- + + @Test + public void testSymlinkInsidePermittedDirIsAllowed() throws Exception { + Path tmpDir = createTempDir(); + buildPolicyPermitting(tmpDir); + + Path realFile = tmpDir.resolve("real.txt"); + Files.writeString(realFile, "data"); + Path symlink = tmpDir.resolve("link.txt"); + + try { + Files.createSymbolicLink(symlink, realFile); + } catch (UnsupportedOperationException | SecurityException e) { + Assume.assumeTrue("Symlinks not supported or permitted in this test environment", false); + return; + } + + // Symlink resolves to a path inside the permitted dir — should NOT throw + FileAccessInterceptor.checkPath( + symlink, "read", SecurityViolationLogger.ViolationType.FILE_READ); + } + + // --------------------------------------------------------------------------- + // Symlink escaping the permitted directory — must be blocked + // --------------------------------------------------------------------------- + + @Test(expected = SecurityException.class) + public void testSymlinkEscapeBlockedInEnforceMode() throws Exception { + Path permittedDir = createTempDir("permitted"); + Path otherDir = createTempDir("other"); + buildPolicyPermitting(permittedDir); + + // Create a real file outside the permitted directory + Path outsideFile = otherDir.resolve("secret.txt"); + Files.writeString(outsideFile, "secret data"); + + // Create a symlink INSIDE the permitted dir that points OUTSIDE it + Path symlink = permittedDir.resolve("escaped.txt"); + try { + Files.createSymbolicLink(symlink, outsideFile); + } catch (UnsupportedOperationException | SecurityException e) { + Assume.assumeTrue("Symlinks not supported or permitted in this test environment", false); + // Satisfy the @Test(expected=SecurityException.class) — fake a throw + throw new SecurityException("symlinks not available — skip"); + } + + // Accessing via the symlink path should be denied because the REAL path is outside permitted + // dir + FileAccessInterceptor.checkPath( + symlink, "read", SecurityViolationLogger.ViolationType.FILE_READ); + } + + @Test + public void testSymlinkEscapeIncrementsFileCounter() throws Exception { + Path permittedDir = createTempDir("permitted"); + Path otherDir = createTempDir("other"); + buildPolicyPermitting(permittedDir); + + Path outsideFile = otherDir.resolve("secret.txt"); + Files.writeString(outsideFile, "secret"); + + Path symlink = permittedDir.resolve("escaped.txt"); + try { + Files.createSymbolicLink(symlink, outsideFile); + } catch (UnsupportedOperationException | SecurityException e) { + Assume.assumeTrue("Symlinks not supported or permitted in this test environment", false); + return; + } + + long before = ViolationMetricsReporter.fileCount(); + try { + FileAccessInterceptor.checkPath( + symlink, "read", SecurityViolationLogger.ViolationType.FILE_READ); + } catch (SecurityException ignored) { + // expected + } + assertEquals(before + 1, ViolationMetricsReporter.fileCount()); + } + + // --------------------------------------------------------------------------- + // Non-existent path — graceful fallback to normalized path check + // --------------------------------------------------------------------------- + + @Test + public void testNonExistentPathFallsBackToNormalizedCheck() { + Path tmpDir = createTempDir(); + buildPolicyPermitting(tmpDir); + + // Non-existent file inside the permitted dir — toRealPath() fails → normalized path used + Path nonExistent = tmpDir.resolve("does-not-exist.txt"); + // Should NOT throw — the normalized path falls within the permitted dir + FileAccessInterceptor.checkPath( + nonExistent, "read", SecurityViolationLogger.ViolationType.FILE_READ); + } +} diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/UncPathRejectionTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/UncPathRejectionTest.java new file mode 100644 index 000000000000..0cfd28333027 --- /dev/null +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/UncPathRejectionTest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import org.apache.solr.SolrTestCase; +import org.junit.Test; + +/** + * Tests that Windows UNC paths ({@code \\server\share\...} or {@code //server/share/...}) are + * detected and rejected regardless of any policy rule. + * + *

UNC path blocking is a platform-independent invariant: even on Linux and macOS the detection + * logic must correctly identify and block UNC-style path strings when they appear in an intercepted + * operation (e.g. via a ByteBuddy-intercepted NIO call on Windows or a crafted path string). + * + *

Note: on POSIX systems Java's {@code Path} normalises {@code //} to {@code /}, so the + * end-to-end enforcement test for forward-slash UNC paths is deferred to Windows CI. These tests + * validate the core detection heuristic which underpins that enforcement. + */ +public class UncPathRejectionTest extends SolrTestCase { + + // --------------------------------------------------------------------------- + // isUncPath detection (unit — platform-independent string check) + // --------------------------------------------------------------------------- + + @Test + public void testBackslashUncDetected() { + assertTrue(FileAccessInterceptor.isUncPath("\\\\server\\share\\file")); + assertTrue(FileAccessInterceptor.isUncPath("\\\\192.168.1.1\\c$\\data")); + assertTrue(FileAccessInterceptor.isUncPath("\\\\?\\Volume{abc}\\file.txt")); + } + + @Test + public void testForwardSlashUncDetected() { + assertTrue(FileAccessInterceptor.isUncPath("//server/share/file")); + assertTrue(FileAccessInterceptor.isUncPath("//192.168.1.1/c$/data/secret")); + } + + @Test + public void testNormalUnixPathsNotDetectedAsUnc() { + assertFalse(FileAccessInterceptor.isUncPath("/normal/path")); + assertFalse(FileAccessInterceptor.isUncPath("/opt/solr/conf")); + assertFalse(FileAccessInterceptor.isUncPath("/tmp/data")); + } + + @Test + public void testWindowsStyleDrivePathNotDetectedAsUnc() { + // Drive-letter paths must NOT be falsely flagged + assertFalse(FileAccessInterceptor.isUncPath("C:\\Windows\\System32")); + assertFalse(FileAccessInterceptor.isUncPath("D:\\data\\backup")); + } + + @Test + public void testRelativePathNotDetectedAsUnc() { + assertFalse(FileAccessInterceptor.isUncPath("relative/path/file.txt")); + assertFalse(FileAccessInterceptor.isUncPath("data/backup")); + } + + @Test + public void testEmptyAndNullSafetyInIsUncPath() { + assertFalse(FileAccessInterceptor.isUncPath("")); + assertFalse(FileAccessInterceptor.isUncPath("/")); + assertFalse(FileAccessInterceptor.isUncPath("\\")); + } +} diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/VirtualThreadCompatibilityTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/VirtualThreadCompatibilityTest.java new file mode 100644 index 000000000000..990b7245c2be --- /dev/null +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/VirtualThreadCompatibilityTest.java @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.solr.SolrTestCase; +import org.junit.After; +import org.junit.Test; + +/** + * Verifies that the Solr security agent enforcement logic is compatible with Java virtual threads + * (Project Loom, available since Java 21). + * + *

{@link StackInspector} uses {@link StackWalker} which is virtual-thread–safe by specification + * (JEP 425). These tests exercise the file and network interceptors from virtual threads to + * confirm: + * + *

    + *
  • Permitted operations succeed from a virtual thread context. + *
  • Denied operations throw {@link SecurityException} and increment counters correctly. + *
  • No {@link NullPointerException} or {@link ClassCastException} arises from stack walking on + * virtual-thread frames. + *
+ */ +public class VirtualThreadCompatibilityTest extends SolrTestCase { + + @After + public void resetSingleton() { + SolrSecurityPolicy.resetForTesting(); + } + + private void resetSingletonSilent() { + SolrSecurityPolicy.resetForTesting(); + } + + /** Runs {@code task} on a virtual thread and re-throws any exception it produces. */ + private static void runOnVirtualThread(RunnableWithException task) throws Exception { + AtomicReference caught = new AtomicReference<>(); + Thread vt = + Thread.ofVirtual() + .start( + () -> { + try { + task.run(); + } catch (Throwable t) { + caught.set(t); + } + }); + vt.join(); + Throwable t = caught.get(); + if (t instanceof Exception e) throw e; + if (t instanceof Error e) throw e; + if (t != null) throw new RuntimeException(t); + } + + @FunctionalInterface + interface RunnableWithException { + void run() throws Exception; + } + + // --------------------------------------------------------------------------- + // File access from virtual threads + // --------------------------------------------------------------------------- + + @Test + public void testPermittedFileAccessFromVirtualThread() throws Exception { + Path tmpDir = createTempDir(); + resetSingletonSilent(); + // Use the real (symlink-resolved) path so the policy matches after toRealPath() resolution + String realDirStr; + try { + realDirStr = tmpDir.toRealPath().toString(); + } catch (java.io.IOException e) { + realDirStr = tmpDir.toAbsolutePath().toString(); + } + PermittedPath allowed = + new PermittedPath(realDirStr, "read", true, PolicyLoader.PolicySource.DEFAULT); + SolrSecurityPolicy policy = + new SolrSecurityPolicy( + List.of(allowed), + List.of(), + List.of(), + List.of(), + SolrSecurityPolicy.EnforcementMode.ENFORCE); + SolrSecurityPolicy.initialize(policy); + + // Create the file so toRealPath() resolves correctly (no fallback to unresolved path) + Path testFile = tmpDir.resolve("test.txt"); + java.nio.file.Files.writeString(testFile, "data"); + + // Must not throw — permitted path from a virtual thread + runOnVirtualThread( + () -> + FileAccessInterceptor.checkPath( + testFile, "read", SecurityViolationLogger.ViolationType.FILE_READ)); + } + + @Test + public void testDeniedFileAccessFromVirtualThreadIncrementsCounter() throws Exception { + resetSingletonSilent(); + SolrSecurityPolicy policy = + new SolrSecurityPolicy( + List.of(), List.of(), List.of(), List.of(), SolrSecurityPolicy.EnforcementMode.ENFORCE); + SolrSecurityPolicy.initialize(policy); + + long before = ViolationMetricsReporter.fileCount(); + try { + runOnVirtualThread( + () -> + FileAccessInterceptor.checkPath( + Path.of("/tmp/denied-vt.txt"), + "read", + SecurityViolationLogger.ViolationType.FILE_READ)); + } catch (SecurityException ignored) { + // expected + } + assertEquals(before + 1, ViolationMetricsReporter.fileCount()); + } + + @Test + public void testDeniedFileAccessFromVirtualThreadNoStackWalkerException() throws Exception { + resetSingletonSilent(); + SolrSecurityPolicy policy = + new SolrSecurityPolicy( + List.of(), List.of(), List.of(), List.of(), SolrSecurityPolicy.EnforcementMode.WARN); + SolrSecurityPolicy.initialize(policy); + + // In warn mode, no SecurityException is thrown, but we must verify no NPE/CCE from StackWalker + AtomicReference unexpected = new AtomicReference<>(); + Thread vt = + Thread.ofVirtual() + .start( + () -> { + try { + FileAccessInterceptor.checkPath( + Path.of("/tmp/vt-check.txt"), + "read", + SecurityViolationLogger.ViolationType.FILE_READ); + } catch (SecurityException ignored) { + // Expected in enforce mode + } catch (Throwable t) { + unexpected.set(t); + } + }); + vt.join(); + assertNull( + "Unexpected exception from virtual-thread stack walk: " + unexpected.get(), + unexpected.get()); + } + + // --------------------------------------------------------------------------- + // Network access from virtual threads + // --------------------------------------------------------------------------- + + @Test + public void testPermittedNetworkFromVirtualThread() throws Exception { + resetSingletonSilent(); + PermittedEndpoint ep = + new PermittedEndpoint("*:8983", "connect,resolve", null, PolicyLoader.PolicySource.DEFAULT); + SolrSecurityPolicy policy = + new SolrSecurityPolicy( + List.of(), + List.of(ep), + List.of(), + List.of(), + SolrSecurityPolicy.EnforcementMode.ENFORCE); + SolrSecurityPolicy.initialize(policy); + + runOnVirtualThread( + () -> + assertTrue( + NetworkAccessInterceptor.isEndpointPermitted( + SolrSecurityPolicy.getInstance(), "10.0.0.1", 8983))); + } + + @Test + public void testDeniedNetworkFromVirtualThreadIncrementsCounter() throws Exception { + resetSingletonSilent(); + SolrSecurityPolicy policy = + new SolrSecurityPolicy( + List.of(), List.of(), List.of(), List.of(), SolrSecurityPolicy.EnforcementMode.ENFORCE); + SolrSecurityPolicy.initialize(policy); + + long before = ViolationMetricsReporter.networkCount(); + try { + runOnVirtualThread( + () -> { + InetSocketAddress addr = + new InetSocketAddress(InetAddress.getByName("10.0.0.99"), 9999); + NetworkAccessInterceptor.checkConnect(addr); + }); + } catch (SecurityException ignored) { + // expected + } + assertEquals(before + 1, ViolationMetricsReporter.networkCount()); + } +} diff --git a/solr/bin/solr b/solr/bin/solr index eaa417ca90d0..c9c2daa85fae 100755 --- a/solr/bin/solr +++ b/solr/bin/solr @@ -1166,6 +1166,24 @@ else SECURITY_MANAGER_OPTS=() fi +# Security agent: detect agent JAR and add -javaagent: if present and not skipped. +# Set SOLR_SECURITY_AGENT_SKIP=true to disable the agent (for troubleshooting only). +AGENT_SM_OPTS=() +if [ "${SOLR_SECURITY_AGENT_SKIP:-false}" != "true" ]; then + AGENT_JAR=$(ls "${SOLR_SERVER_DIR}/lib/ext/solr-agent-sm-"*.jar 2>/dev/null | head -1) + if [ -n "$AGENT_JAR" ]; then + AGENT_SM_OPTS+=("-javaagent:${AGENT_JAR}") + # Convert env vars to JVM system properties so the agent can read them via System.getProperty() + # (the agent JAR has no dependency on solr:core and cannot use EnvUtils). + if [ -n "${SOLR_SECURITY_AGENT_MODE:-}" ]; then + AGENT_SM_OPTS+=("-Dsolr.security.agent.mode=${SOLR_SECURITY_AGENT_MODE}") + fi + if [ -n "${SOLR_SECURITY_AGENT_EXTRA_POLICY:-}" ]; then + AGENT_SM_OPTS+=("-Dsolr.security.agent.extra.policy=${SOLR_SECURITY_AGENT_EXTRA_POLICY}") + fi + fi +fi + JAVA_MEM_OPTS=() if [ -z "${SOLR_HEAP:-}" ] && [ -n "${SOLR_JAVA_MEM:-}" ]; then JAVA_MEM_OPTS=($SOLR_JAVA_MEM) @@ -1315,7 +1333,7 @@ function start_solr() { # OOME is thrown. Program operation after OOME is unpredictable. "-XX:+CrashOnOutOfMemoryError" "-XX:ErrorFile=${SOLR_LOGS_DIR}/jvm_crash_%p.log" \ "-Djetty.home=$SOLR_SERVER_DIR" "-Dsolr.solr.home=$SOLR_HOME" "-Dsolr.install.dir=$SOLR_TIP" "-Dsolr.install.symDir=$SOLR_TIP_SYM" \ - "${LOG4J_CONFIG[@]}" "${SCRIPT_SOLR_OPTS[@]}" "${SECURITY_MANAGER_OPTS[@]}" "${SOLR_OPTS[@]}") + "${AGENT_SM_OPTS[@]}" "${LOG4J_CONFIG[@]}" "${SCRIPT_SOLR_OPTS[@]}" "${SECURITY_MANAGER_OPTS[@]}" "${SOLR_OPTS[@]}") mk_writable_dir "$SOLR_LOGS_DIR" "Logs" if [[ -n "${SOLR_HEAP_DUMP_DIR:-}" ]]; then diff --git a/solr/bin/solr.cmd b/solr/bin/solr.cmd index e30cc4416fa8..cd0511adc2d1 100755 --- a/solr/bin/solr.cmd +++ b/solr/bin/solr.cmd @@ -1021,6 +1021,18 @@ IF "%GC_TUNE%"=="" ( -XX:+ExplicitGCInvokesConcurrent ) +REM Security agent: detect agent JAR and add -javaagent: if present and not skipped. +REM Set SOLR_SECURITY_AGENT_SKIP=true to disable the agent (for troubleshooting only). +set AGENT_SM_OPTS= +IF NOT "%SOLR_SECURITY_AGENT_SKIP%"=="true" ( + IF EXIST "%SOLR_SERVER_DIR%\lib\ext\solr-agent-sm-*.jar" ( + FOR %%F IN ("%SOLR_SERVER_DIR%\lib\ext\solr-agent-sm-*.jar") DO SET "AGENT_JAR=%%F" + SET "AGENT_SM_OPTS=-javaagent:!AGENT_JAR!" + IF DEFINED SOLR_SECURITY_AGENT_MODE SET "AGENT_SM_OPTS=!AGENT_SM_OPTS! -Dsolr.security.agent.mode=!SOLR_SECURITY_AGENT_MODE!" + IF DEFINED SOLR_SECURITY_AGENT_EXTRA_POLICY SET "AGENT_SM_OPTS=!AGENT_SM_OPTS! -Dsolr.security.agent.extra.policy=!SOLR_SECURITY_AGENT_EXTRA_POLICY!" + ) +) + REM Add vector optimizations module set SCRIPT_SOLR_OPTS=%SCRIPT_SOLR_OPTS% --add-modules jdk.incubator.vector @@ -1096,6 +1108,7 @@ IF NOT "%SOLR_ADDL_ARGS%"=="" set "START_OPTS=%START_OPTS% %SOLR_ADDL_ARGS%" IF NOT "%SOLR_HOST_ADVERTISE_ARG%"=="" set "START_OPTS=%START_OPTS% %SOLR_HOST_ADVERTISE_ARG%" IF NOT "%SCRIPT_SOLR_OPTS%"=="" set "START_OPTS=%START_OPTS% %SCRIPT_SOLR_OPTS%" IF NOT "%SOLR_OPTS_INTERNAL%"=="" set "START_OPTS=%START_OPTS% %SOLR_OPTS_INTERNAL%" +IF NOT "!AGENT_SM_OPTS!"=="" set "START_OPTS=!AGENT_SM_OPTS! %START_OPTS%" IF NOT "!SECURITY_MANAGER_OPTS!"=="" set "START_OPTS=%START_OPTS% !SECURITY_MANAGER_OPTS!" IF "%SOLR_SSL_ENABLED%"=="true" ( set "SSL_PORT_PROP=-Dsolr.jetty.https.port=%SOLR_PORT_LISTEN%" diff --git a/solr/bin/solr.in.cmd b/solr/bin/solr.in.cmd index 81be993aa47b..4a4fbd36643a 100755 --- a/solr/bin/solr.in.cmd +++ b/solr/bin/solr.in.cmd @@ -221,6 +221,23 @@ REM You can also tweak via standard JDK files such as ~\.java.policy, see https: REM This is experimental! REM set SOLR_SECURITY_MANAGER_ENABLED=true +REM --- Solr Security Agent --- +REM The Solr security agent replaces the deprecated Java Security Manager with ByteBuddy-based +REM enforcement. The agent JAR is auto-detected at server\lib\ext\solr-agent-sm-*.jar and activated +REM automatically. See the Solr Reference Guide for full documentation. + +REM Enforcement mode: "warn" (log violations, allow operation — default) or +REM "enforce" (log violations, block operation with SecurityException). +REM The startup script converts this env var to -Dsolr.security.agent.mode for the agent. +REM set SOLR_SECURITY_AGENT_MODE=warn + +REM Set to "true" to completely disable the security agent (for troubleshooting only). +REM set SOLR_SECURITY_AGENT_SKIP=false + +REM Path to the operator extension policy file (default: server\etc\agent-security-extra.policy). +REM The startup script converts this env var to -Dsolr.security.agent.extra.policy for the agent. +REM set SOLR_SECURITY_AGENT_EXTRA_POLICY=C:\path\to\custom-agent-security-extra.policy + REM This variable provides you with the option to disable the Admin UI. if you uncomment the variable below and REM change the value to false. The option is configured as a system property as defined in SOLR_START_OPTS in the start REM scripts. diff --git a/solr/bin/solr.in.sh b/solr/bin/solr.in.sh index 569066fede45..f78dc61101a4 100644 --- a/solr/bin/solr.in.sh +++ b/solr/bin/solr.in.sh @@ -247,6 +247,26 @@ # This is experimental! #SOLR_SECURITY_MANAGER_ENABLED=true +# --- Solr Security Agent --- +# The Solr security agent replaces the deprecated Java Security Manager with ByteBuddy-based +# enforcement. The agent JAR is auto-detected at server/lib/ext/solr-agent-sm-*.jar and activated +# automatically. See the Solr Reference Guide for full documentation. + +# Enforcement mode: "warn" (log violations, allow operation — default) or +# "enforce" (log violations, block operation with SecurityException). +# The startup script converts this env var to -Dsolr.security.agent.mode for the agent. +#SOLR_SECURITY_AGENT_MODE=warn + +# Set to "true" to completely disable the security agent (for troubleshooting only). +# Without the agent, no security controls are active. +#SOLR_SECURITY_AGENT_SKIP=false + +# Path to the operator extension policy file. +# Defaults to ${server.dir}/etc/agent-security-extra.policy when unset. +# Override to support read-only install trees, container images, or config-management tooling. +# The startup script converts this env var to -Dsolr.security.agent.extra.policy for the agent. +#SOLR_SECURITY_AGENT_EXTRA_POLICY=/path/to/custom-agent-security-extra.policy + # This variable provides you with the option to disable the Admin UI. If you uncomment the variable below and # change the value to false. The option is configured as a system property as defined in SOLR_START_OPTS in the start # scripts. diff --git a/solr/core/gradle.lockfile b/solr/core/gradle.lockfile index 7267ab45b84a..c41b089ecf4d 100644 --- a/solr/core/gradle.lockfile +++ b/solr/core/gradle.lockfile @@ -1,194 +1,194 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. -biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,testCompileClasspath -com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.4=jarValidation,testCompileClasspath,testRuntimeClasspath -com.carrotsearch:hppc:0.10.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -com.fasterxml.jackson.core:jackson-annotations:2.21=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -com.fasterxml.jackson.core:jackson-core:2.21.2=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -com.fasterxml.jackson.core:jackson-databind:2.21.2=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.21.2=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.21.2=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.21.2=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -com.fasterxml.jackson:jackson-bom:2.21.2=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -com.fasterxml.woodstox:woodstox-core:7.0.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,compileClasspath,errorprone,jarValidation,runtimeClasspath,runtimeLibs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,compileClasspathCopy,testCompileClasspath,testCompileClasspathCopy +com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.4=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.carrotsearch:hppc:0.10.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.core:jackson-annotations:2.21=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.core:jackson-core:2.21.2=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.core:jackson-databind:2.21.2=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.21.2=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.21.2=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.21.2=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson:jackson-bom:2.21.2=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.woodstox:woodstox-core:7.0.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,errorprone,testAnnotationProcessor com.github.spotbugs:spotbugs-annotations:4.9.8=compileClasspath,compileOnlyHelper,compileOnlyHelperTest,testCompileClasspath -com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,compileOnlyHelper,jarValidation +com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,compileClasspathCopy,compileOnlyHelper,jarValidation com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,errorprone,testAnnotationProcessor com.google.auto.value:auto-value-annotations:1.11.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.auto:auto-common:1.2.2=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_annotation:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor -com.google.errorprone:error_prone_annotations:2.41.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.41.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy com.google.errorprone:error_prone_annotations:2.43.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_check_api:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_core:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.googlejavaformat:google-java-format:1.27.0=annotationProcessor,errorprone,testAnnotationProcessor -com.google.guava:failureaccess:1.0.3=annotationProcessor,apiHelper,compileClasspath,errorprone,jarValidation,runtimeClasspath,runtimeLibs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath -com.google.guava:guava:33.5.0-jre=annotationProcessor,apiHelper,compileClasspath,errorprone,jarValidation,runtimeClasspath,runtimeLibs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath -com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,apiHelper,compileClasspath,errorprone,jarValidation,runtimeClasspath,runtimeLibs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath -com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,apiHelper,compileClasspath,errorprone,jarValidation,runtimeClasspath,runtimeLibs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.3=annotationProcessor,apiHelper,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.google.guava:guava:33.5.0-jre=annotationProcessor,apiHelper,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,apiHelper,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,apiHelper,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy com.google.protobuf:protobuf-java:3.25.8=annotationProcessor,errorprone,testAnnotationProcessor -com.ibm.icu:icu4j:77.1=jarValidation,testRuntimeClasspath -com.j256.simplemagic:simplemagic:1.17=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -com.jayway.jsonpath:json-path:2.9.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -com.microsoft.onnxruntime:onnxruntime:1.24.3=jarValidation,testRuntimeClasspath -com.tdunning:t-digest:3.3=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -commons-cli:commons-cli:1.11.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -commons-codec:commons-codec:1.21.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -commons-io:commons-io:2.21.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.dropwizard.metrics:metrics-core:3.2.5=jarValidation,testCompileClasspath,testRuntimeClasspath +com.ibm.icu:icu4j:77.1=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +com.j256.simplemagic:simplemagic:1.17=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.jayway.jsonpath:json-path:2.9.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.microsoft.onnxruntime:onnxruntime:1.24.3=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +com.tdunning:t-digest:3.3=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +commons-cli:commons-cli:1.11.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +commons-codec:commons-codec:1.21.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +commons-io:commons-io:2.21.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.dropwizard.metrics:metrics-core:3.2.5=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,errorprone,testAnnotationProcessor io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,errorprone,testAnnotationProcessor -io.netty:netty-buffer:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.netty:netty-codec-base:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.netty:netty-common:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.netty:netty-handler:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.netty:netty-resolver:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.netty:netty-tcnative-boringssl-static:2.0.75.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.netty:netty-tcnative-classes:2.0.75.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.netty:netty-transport-classes-epoll:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.netty:netty-transport-native-epoll:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.netty:netty-transport-native-unix-common:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.netty:netty-transport:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator:2.22.0-alpha=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:2.22.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java17:2.22.0-alpha=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java8:2.22.0-alpha=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -io.opentelemetry.semconv:opentelemetry-semconv:1.37.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-api-incubator:1.56.0-alpha=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-api:1.56.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-common:1.56.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-context:1.56.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-exporter-prometheus:1.56.0-alpha=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-common:1.56.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-logs:1.56.0=jarValidation,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-metrics:1.56.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-testing:1.56.0=jarValidation,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-trace:1.56.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk:1.56.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.prometheus:prometheus-metrics-exposition-formats:1.1.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.prometheus:prometheus-metrics-model:1.1.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.sgr:s2-geometry-library-java:1.0.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -io.swagger.core.v3:swagger-annotations-jakarta:2.2.22=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -jakarta.activation:jakarta.activation-api:2.1.3=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -jakarta.servlet:jakarta.servlet-api:6.1.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -jakarta.validation:jakarta.validation-api:3.1.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath +io.netty:netty-buffer:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-codec-base:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-common:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-handler:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-resolver:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-tcnative-boringssl-static:2.0.75.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-tcnative-classes:2.0.75.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-transport-classes-epoll:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-transport-native-epoll:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-transport-native-unix-common:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-transport:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator:2.22.0-alpha=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:2.22.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java17:2.22.0-alpha=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java8:2.22.0-alpha=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.semconv:opentelemetry-semconv:1.37.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-api-incubator:1.56.0-alpha=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-api:1.56.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-common:1.56.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-context:1.56.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-exporter-prometheus:1.56.0-alpha=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk-common:1.56.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk-logs:1.56.0=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk-metrics:1.56.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk-testing:1.56.0=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk-trace:1.56.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk:1.56.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.prometheus:prometheus-metrics-exposition-formats:1.1.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.prometheus:prometheus-metrics-model:1.1.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.sgr:s2-geometry-library-java:1.0.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.swagger.core.v3:swagger-annotations-jakarta:2.2.22=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.activation:jakarta.activation-api:2.1.3=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.servlet:jakarta.servlet-api:6.1.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.validation:jakarta.validation-api:3.1.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy jakarta.ws.rs:jakarta.ws.rs-api:3.1.0=apiHelper -jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath +jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor -junit:junit:4.13.2=jarValidation,testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy:1.18.8-jdk5=jarValidation,testCompileClasspath,testRuntimeClasspath -org.antlr:antlr4-runtime:4.13.2=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.apache.commons:commons-exec:1.6.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.commons:commons-lang3:3.20.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.commons:commons-math3:3.6.1=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.curator:curator-client:5.9.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.curator:curator-framework:5.9.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.curator:curator-test:5.9.0=jarValidation,testCompileClasspath,testRuntimeClasspath -org.apache.httpcomponents:httpclient:4.5.14=jarValidation,testCompileClasspath,testRuntimeClasspath -org.apache.httpcomponents:httpcore:4.4.16=jarValidation,testCompileClasspath,testRuntimeClasspath -org.apache.httpcomponents:httpmime:4.5.14=jarValidation,testRuntimeClasspath -org.apache.logging.log4j:log4j-api:2.25.3=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.logging.log4j:log4j-core:2.25.3=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.logging.log4j:log4j-slf4j2-impl:2.25.3=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.apache.lucene:lucene-analysis-common:10.4.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-analysis-icu:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-analysis-kuromoji:10.4.0=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.apache.lucene:lucene-analysis-morfologik:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-analysis-nori:10.4.0=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.apache.lucene:lucene-analysis-opennlp:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-analysis-phonetic:10.4.0=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.apache.lucene:lucene-analysis-smartcn:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-analysis-stempel:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-backward-codecs:10.4.0=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.apache.lucene:lucene-classification:10.4.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-codecs:10.4.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-core:10.4.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-expressions:10.4.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-facet:10.4.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-grouping:10.4.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-highlighter:10.4.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-join:10.4.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-memory:10.4.0=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.apache.lucene:lucene-misc:10.4.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-queries:10.4.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-queryparser:10.4.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-sandbox:10.4.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-spatial-extras:10.4.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-spatial3d:10.4.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-suggest:10.4.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-test-framework:10.4.0=jarValidation,testCompileClasspath,testRuntimeClasspath -org.apache.opennlp:opennlp-dl:2.5.8=jarValidation,testRuntimeClasspath -org.apache.opennlp:opennlp-tools:2.5.8=jarValidation,testRuntimeClasspath -org.apache.zookeeper:zookeeper-jute:3.9.4=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apache.zookeeper:zookeeper:3.9.4=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.apiguardian:apiguardian-api:1.1.2=jarValidation,testCompileClasspath,testRuntimeClasspath -org.carrot2:morfologik-fsa:2.1.9=jarValidation,testRuntimeClasspath -org.carrot2:morfologik-polish:2.1.9=jarValidation,testRuntimeClasspath -org.carrot2:morfologik-stemming:2.1.9=jarValidation,testRuntimeClasspath -org.codehaus.woodstox:stax2-api:4.2.2=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.34=jarValidation,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:jetty-http2-client-transport:12.0.34=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.eclipse.jetty.http2:jetty-http2-client:12.0.34=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:jetty-http2-common:12.0.34=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:jetty-http2-hpack:12.0.34=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:jetty-http2-server:12.0.34=jarValidation,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-client:12.0.34=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-java-client:12.0.34=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-java-server:12.0.34=jarValidation,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-server:12.0.34=jarValidation,testRuntimeClasspath -org.eclipse.jetty:jetty-client:12.0.34=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-http:12.0.34=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-io:12.0.34=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-rewrite:12.0.34=jarValidation,testRuntimeClasspath -org.eclipse.jetty:jetty-security:12.0.34=jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-server:12.0.34=jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-session:12.0.34=jarValidation,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-util:12.0.34=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.glassfish.hk2.external:aopalliance-repackaged:3.1.1=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.glassfish.hk2.external:aopalliance-repackaged:4.0.0-M3=runtimeLibs -org.glassfish.hk2:hk2-api:3.1.1=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.glassfish.hk2:hk2-api:4.0.0-M3=runtimeLibs -org.glassfish.hk2:hk2-locator:4.0.0-M3=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.glassfish.hk2:hk2-utils:3.1.1=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.glassfish.hk2:hk2-utils:4.0.0-M3=runtimeLibs -org.glassfish.hk2:osgi-resource-locator:3.0.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.glassfish.jersey.containers:jersey-container-jetty-http:4.0.2=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.glassfish.jersey.core:jersey-client:4.0.2=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.glassfish.jersey.core:jersey-common:4.0.2=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.glassfish.jersey.core:jersey-server:4.0.2=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.glassfish.jersey.ext:jersey-entity-filtering:4.0.2=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.glassfish.jersey.inject:jersey-hk2:4.0.2=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.glassfish.jersey.media:jersey-media-json-jackson:4.0.2=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.glassfish.jersey:jersey-bom:4.0.2=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:3.0=jarValidation,testCompileClasspath,testRuntimeClasspath -org.javassist:javassist:3.30.2-GA=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.jspecify:jspecify:1.0.0=annotationProcessor,apiHelper,compileClasspath,errorprone,jarValidation,runtimeClasspath,runtimeLibs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.6.2=jarValidation,testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.6.2=jarValidation,testCompileClasspath,testRuntimeClasspath +junit:junit:4.13.2=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +net.bytebuddy:byte-buddy:1.18.8-jdk5=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.antlr:antlr4-runtime:4.13.2=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.commons:commons-exec:1.6.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.commons:commons-lang3:3.20.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.commons:commons-math3:3.6.1=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.curator:curator-client:5.9.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.curator:curator-framework:5.9.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.curator:curator-test:5.9.0=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.httpcomponents:httpclient:4.5.14=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.httpcomponents:httpcore:4.4.16=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.httpcomponents:httpmime:4.5.14=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.logging.log4j:log4j-api:2.25.3=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.logging.log4j:log4j-core:2.25.3=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.logging.log4j:log4j-slf4j2-impl:2.25.3=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-common:10.4.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-icu:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-kuromoji:10.4.0=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-morfologik:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-nori:10.4.0=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-opennlp:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-phonetic:10.4.0=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-smartcn:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-stempel:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-backward-codecs:10.4.0=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-classification:10.4.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-codecs:10.4.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-core:10.4.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-expressions:10.4.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-facet:10.4.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-grouping:10.4.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-highlighter:10.4.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-join:10.4.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-memory:10.4.0=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-misc:10.4.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-queries:10.4.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-queryparser:10.4.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-sandbox:10.4.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-spatial-extras:10.4.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-spatial3d:10.4.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-suggest:10.4.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-test-framework:10.4.0=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.opennlp:opennlp-dl:2.5.8=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.opennlp:opennlp-tools:2.5.8=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.zookeeper:zookeeper-jute:3.9.4=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.zookeeper:zookeeper:3.9.4=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apiguardian:apiguardian-api:1.1.2=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.carrot2:morfologik-fsa:2.1.9=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.carrot2:morfologik-polish:2.1.9=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.carrot2:morfologik-stemming:2.1.9=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.codehaus.woodstox:stax2-api:4.2.2=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.34=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-client-transport:12.0.34=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-client:12.0.34=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-common:12.0.34=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-hpack:12.0.34=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-server:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-client:12.0.34=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-java-client:12.0.34=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-java-server:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-server:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-client:12.0.34=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-http:12.0.34=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-io:12.0.34=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-rewrite:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-security:12.0.34=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-server:12.0.34=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-session:12.0.34=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-util:12.0.34=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2.external:aopalliance-repackaged:3.1.1=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath +org.glassfish.hk2.external:aopalliance-repackaged:4.0.0-M3=runtimeClasspathCopy,runtimeLibs,testRuntimeClasspathCopy +org.glassfish.hk2:hk2-api:3.1.1=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath +org.glassfish.hk2:hk2-api:4.0.0-M3=runtimeClasspathCopy,runtimeLibs,testRuntimeClasspathCopy +org.glassfish.hk2:hk2-locator:4.0.0-M3=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2:hk2-utils:3.1.1=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath +org.glassfish.hk2:hk2-utils:4.0.0-M3=runtimeClasspathCopy,runtimeLibs,testRuntimeClasspathCopy +org.glassfish.hk2:osgi-resource-locator:3.0.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.containers:jersey-container-jetty-http:4.0.2=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.core:jersey-client:4.0.2=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.core:jersey-common:4.0.2=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.core:jersey-server:4.0.2=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.ext:jersey-entity-filtering:4.0.2=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.inject:jersey-hk2:4.0.2=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.media:jersey-media-json-jackson:4.0.2=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey:jersey-bom:4.0.2=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.hamcrest:hamcrest:3.0=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.javassist:javassist:3.30.2-GA=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.jspecify:jspecify:1.0.0=annotationProcessor,apiHelper,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.junit.jupiter:junit-jupiter-api:5.6.2=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.junit.platform:junit-platform-commons:1.6.2=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy org.junit:junit-bom:5.14.0=compileOnlyHelper,compileOnlyHelperTest -org.junit:junit-bom:5.6.2=jarValidation,testCompileClasspath,testRuntimeClasspath -org.locationtech.spatial4j:spatial4j:0.8=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.mockito:mockito-core:5.23.0=jarValidation,testCompileClasspath,testRuntimeClasspath -org.mockito:mockito-subclass:5.23.0=jarValidation,testRuntimeClasspath -org.objenesis:objenesis:3.3=jarValidation,testRuntimeClasspath -org.opentest4j:opentest4j:1.2.0=jarValidation,testCompileClasspath,testRuntimeClasspath -org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,testCompileClasspath -org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,testCompileClasspath -org.osgi:org.osgi.resource:1.0.0=compileClasspath,testCompileClasspath -org.osgi:org.osgi.service.serviceloader:1.0.0=compileClasspath,testCompileClasspath -org.ow2.asm:asm-commons:9.8=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.ow2.asm:asm-tree:9.8=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.ow2.asm:asm:9.8=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath +org.junit:junit-bom:5.6.2=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.locationtech.spatial4j:spatial4j:0.8=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.mockito:mockito-core:5.23.0=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.mockito:mockito-subclass:5.23.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.objenesis:objenesis:3.3=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.opentest4j:opentest4j:1.2.0=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,compileClasspathCopy,testCompileClasspath,testCompileClasspathCopy +org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,compileClasspathCopy,testCompileClasspath,testCompileClasspathCopy +org.osgi:org.osgi.resource:1.0.0=compileClasspath,compileClasspathCopy,testCompileClasspath,testCompileClasspathCopy +org.osgi:org.osgi.service.serviceloader:1.0.0=compileClasspath,compileClasspathCopy,testCompileClasspath,testCompileClasspathCopy +org.ow2.asm:asm-commons:9.8=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.ow2.asm:asm-tree:9.8=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.ow2.asm:asm:9.8=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy org.pcollections:pcollections:4.0.1=annotationProcessor,errorprone,testAnnotationProcessor -org.semver4j:semver4j:6.0.0=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.slf4j:jcl-over-slf4j:2.0.17=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath -org.slf4j:slf4j-api:2.0.17=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -org.xerial.snappy:snappy-java:1.1.10.8=jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -ua.net.nlp:morfologik-ukrainian-search:4.9.1=jarValidation,testRuntimeClasspath +org.semver4j:semver4j:6.0.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.slf4j:jcl-over-slf4j:2.0.17=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testRuntimeClasspath,testRuntimeClasspathCopy +org.slf4j:slf4j-api:2.0.17=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.xerial.snappy:snappy-java:1.1.10.8=jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +ua.net.nlp:morfologik-ukrainian-search:4.9.1=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy empty=apiHelperTest,missingdoclet,permitAggregatorUse,permitTestAggregatorUse,permitTestUnusedDeclared,permitTestUsedUndeclared,permitUnusedDeclared,permitUsedUndeclared,signatures diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java index 186e319eae89..50866a8fdb18 100644 --- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java +++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java @@ -951,6 +951,21 @@ private void loadInternal() { fieldCacheBean.initializeMetrics( solrMetricsContext, Attributes.of(CATEGORY_ATTR, SolrInfoBean.Category.CACHE.toString())); + // Register security agent violation metrics if the agent is loaded. + // Uses reflection to avoid a compile-time dependency on solr:agent-sm (see research.md Decision + // 8). + try { + Class reporter = + Class.forName("org.apache.solr.security.agent.ViolationMetricsReporter", false, null); + reporter + .getMethod("registerWithSolrMetrics", Object.class, String.class) + .invoke(null, metricManager, NODE_REGISTRY); + } catch (ClassNotFoundException ignored) { + // Agent not loaded (e.g. SOLR_SECURITY_AGENT_SKIP=true); metrics registration skipped. + } catch (ReflectiveOperationException e) { + log.warn("Failed to register security agent metrics", e); + } + // setup executor to load cores in parallel coreLoadExecutor = solrMetricsContext.instrumentedExecutorService( diff --git a/solr/core/src/java/org/apache/solr/core/SolrPaths.java b/solr/core/src/java/org/apache/solr/core/SolrPaths.java index dc2c245eada1..4e61f6f03a32 100644 --- a/solr/core/src/java/org/apache/solr/core/SolrPaths.java +++ b/solr/core/src/java/org/apache/solr/core/SolrPaths.java @@ -61,7 +61,10 @@ public static String normalizeDir(String path) { * @param pathToAssert path to check * @param allowPaths list of paths that should be allowed prefixes for pathToAssert * @throws SolrException if path is outside allowed paths + * @deprecated Automatic enforcement via the security agent supersedes this check; do not add new + * call sites. Existing call sites are retained as defense-in-depth. */ + @Deprecated public static void assertPathAllowed(Path pathToAssert, Set allowPaths) throws SolrException { if (ALL_PATHS.equals(allowPaths)) return; // Catch-all allows all paths (*/_ALL_) diff --git a/solr/server/etc/agent-security-extra.policy b/solr/server/etc/agent-security-extra.policy new file mode 100644 index 000000000000..c0a77643836c --- /dev/null +++ b/solr/server/etc/agent-security-extra.policy @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Solr Security Agent — operator extension policy +// +// This file is loaded in addition to the bundled default policy (agent-security.policy). +// Add entries here to permit additional file paths or network endpoints required by your +// specific deployment, plugins, or custom components. +// +// This file's location can be overridden via: +// - Environment variable: SOLR_SECURITY_AGENT_EXTRA_POLICY=/path/to/custom.policy +// - System property: -Dsolr.security.agent.extra.policy=/path/to/custom.policy +// +// An absent file is silently skipped — it is not a startup error. +// +// Syntax: JDK-style .policy files (same as agent-security.policy). +// Supported variables: ${solr.home}, ${solr.data.dir}, ${solr.log.dir}, ${solr.install.dir}, +// ${java.io.tmpdir}, ${java.home}, ${solr.port}, ${solr.zk.port} +// +// For documentation and examples, see the Solr Reference Guide: Security Agent. +// +// --------------------------------------------------------------------------- +// EXAMPLES (uncomment and adapt as needed) +// --------------------------------------------------------------------------- + +// Example: permit a plugin that reads from an NFS mount +// grant { +// permission java.io.FilePermission "/mnt/nfs-data/-", "read"; +// }; + +// Example: permit a plugin that connects to an internal analytics service +// grant { +// permission java.net.SocketPermission "analytics.internal:443", "connect,resolve"; +// }; + +// Example: extraction module — remote Tika Server (see NOTICE in the extraction module docs) +// WARNING: Use a specific hostname:port, not a wildcard, to prevent SSRF attacks. +// grant { +// permission java.net.SocketPermission "tika-server.internal:9998", "connect,resolve"; +// }; diff --git a/solr/server/etc/agent-security.policy b/solr/server/etc/agent-security.policy new file mode 100644 index 000000000000..828b50617b8c --- /dev/null +++ b/solr/server/etc/agent-security.policy @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Solr Security Agent — default production policy +// +// This file is the bundled default policy for the Solr security agent. It should not be edited +// by operators. To grant additional permissions for your deployment, use the operator extension +// file at: ${server.dir}/etc/agent-security-extra.policy (or the path set by +// SOLR_SECURITY_AGENT_EXTRA_POLICY / -Dsolr.security.agent.extra.policy). +// +// Syntax: JDK-style .policy files with Solr variable substitution. +// Supported variables: ${solr.home}, ${solr.data.dir}, ${solr.log.dir}, ${solr.install.dir}, +// ${java.io.tmpdir}, ${java.home}, ${solr.port}, ${solr.zk.port} +// +// For documentation see the Solr Reference Guide: Security Agent. + +// --------------------------------------------------------------------------- +// Global grant — applies to all code running in the Solr JVM +// --------------------------------------------------------------------------- +grant { + + // --- File system access --- + + // Solr home: read-only (config files, schema, solrconfig.xml, etc.) + permission java.io.FilePermission "${solr.home}/-", "read"; + + // Solr data and index directories: full read/write/delete + permission java.io.FilePermission "${solr.data.dir}/-", "read,write,delete"; + + // Log directory: write access for log files + permission java.io.FilePermission "${solr.log.dir}/-", "read,write,delete"; + + // Temporary files: full access (sort buffers, merged segments, etc.) + permission java.io.FilePermission "${java.io.tmpdir}/-", "read,write,delete"; + + // JDK runtime libraries: read-only (class loading, native libs, etc.) + permission java.io.FilePermission "${java.home}/-", "read"; + + // --- Outbound network access --- + + // Loopback addresses: unconditionally permitted (inter-thread, localhost HTTP, embedded ZK) + permission java.net.SocketPermission "localhost:1-65535", "connect,resolve"; + permission java.net.SocketPermission "127.0.0.1:1-65535", "connect,resolve"; + permission java.net.SocketPermission "[::1]:1-65535", "connect,resolve"; + + // Intra-cluster connectivity — Decision 9 (port-wildcard approach): + // Any host on the Solr HTTP port is permitted, covering all current and future cluster nodes + // without requiring a static node list at startup. Similarly for the ZooKeeper port. + permission java.net.SocketPermission "*:${solr.port}", "connect,resolve"; + permission java.net.SocketPermission "*:${solr.zk.port}", "connect,resolve"; + +}; + +// --------------------------------------------------------------------------- +// Per-module codeBase grants for bundled modules that make outbound network calls +// +// Each of the five pre-permitted modules below has its external endpoint controlled exclusively +// by a node or cluster administrator (not by collection-level config), making a wildcard grant +// safe from SSRF abuse. +// +// NOTE: The 'extraction' module is intentionally excluded because its Tika Server URL is +// configurable at CONFIG_EDIT privilege level (collection admin). Operators using remote Tika +// must add an explicit host-locked entry in agent-security-extra.policy. See the extraction +// module documentation for a ready-to-paste example. +// --------------------------------------------------------------------------- + +// jwt-auth: OIDC/JWKS endpoint configured via security.json (requires Solr SECURITY_EDIT admin) +grant codeBase "file:${solr.install.dir}/modules/jwt-auth/-" { + permission java.net.SocketPermission "*", "connect,resolve"; +}; + +// opentelemetry: OTLP collector endpoint configured via env var or solr.xml (node admin only) +grant codeBase "file:${solr.install.dir}/modules/opentelemetry/-" { + permission java.net.SocketPermission "*", "connect,resolve"; +}; + +// s3-repository: Amazon S3 / S3-compatible endpoint configured via solr.xml backup handler (node admin) +grant codeBase "file:${solr.install.dir}/modules/s3-repository/-" { + permission java.net.SocketPermission "*", "connect,resolve"; +}; + +// gcs-repository: Google Cloud Storage endpoint configured via solr.xml backup handler (node admin) +grant codeBase "file:${solr.install.dir}/modules/gcs-repository/-" { + permission java.net.SocketPermission "*", "connect,resolve"; +}; + +// cross-dc-manager: Apache Kafka brokers configured via env var / system property / ZooKeeper (admin) +grant codeBase "file:${solr.install.dir}/modules/cross-dc-manager/-" { + permission java.net.SocketPermission "*", "connect,resolve"; +}; diff --git a/solr/server/etc/security.policy b/solr/server/etc/security.policy index f932cc0b461c..14f63e76667d 100644 --- a/solr/server/etc/security.policy +++ b/solr/server/etc/security.policy @@ -15,6 +15,11 @@ * limitations under the License. */ +// DEPRECATED: This file is no longer enforced by the JVM. The Java Security Manager (JSM) API +// was removed in JDK 24. This file is retained as a migration reference only and will be removed +// in a future Solr release. The replacement is agent-security.policy, loaded by the Solr security +// agent (solr-agent-sm-*.jar). See the Solr Reference Guide: Security Agent for details. + // Policy file for solr. Please keep minimal and avoid wildcards. // permissions needed for tests to pass, based on properties set by the build system diff --git a/solr/solr-ref-guide/gradle.lockfile b/solr/solr-ref-guide/gradle.lockfile index 35a6de3f6353..f2efab787693 100644 --- a/solr/solr-ref-guide/gradle.lockfile +++ b/solr/solr-ref-guide/gradle.lockfile @@ -176,4 +176,4 @@ org.semver4j:semver4j:6.0.0=testRuntimeClasspath org.slf4j:jcl-over-slf4j:2.0.17=testRuntimeClasspath org.slf4j:slf4j-api:2.0.17=testCompileClasspath,testRuntimeClasspath org.xerial.snappy:snappy-java:1.1.10.8=testRuntimeClasspath -empty=apiHelper,apiHelperTest,compileClasspath,compileOnlyHelper,compileOnlyHelperTest,jarValidation,localPlaybook,missingdoclet,officialPlaybook,permitAggregatorUse,permitTestAggregatorUse,permitTestUnusedDeclared,permitTestUsedUndeclared,permitUnusedDeclared,permitUsedUndeclared,refGuide,runtimeClasspath +empty=apiHelper,apiHelperTest,compileClasspath,compileClasspathCopy,compileOnlyHelper,compileOnlyHelperTest,jarValidation,localPlaybook,missingdoclet,officialPlaybook,permitAggregatorUse,permitTestAggregatorUse,permitTestUnusedDeclared,permitTestUsedUndeclared,permitUnusedDeclared,permitUsedUndeclared,refGuide,runtimeClasspath,runtimeClasspathCopy,testCompileClasspathCopy,testRuntimeClasspathCopy diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/solr-properties.adoc b/solr/solr-ref-guide/modules/configuration-guide/pages/solr-properties.adoc index b9ecaddb1002..e473b5d0922e 100644 --- a/solr/solr-ref-guide/modules/configuration-guide/pages/solr-properties.adoc +++ b/solr/solr-ref-guide/modules/configuration-guide/pages/solr-properties.adoc @@ -92,6 +92,12 @@ NOTE: Properties marked with "!" indicate inverted meaning between pre Solr 10 a |solr.responses.stacktrace.enabled|!solr.hideStackTrace|false|Controls whether stack traces are included in responses. When set to `true`, stack traces are included in responses. +|solr.security.agent.extra.policy|SOLR_SECURITY_AGENT_EXTRA_POLICY|`${server.dir}/etc/agent-security-extra.policy`|Path to the operator extension policy file for the Solr security agent. Overrides the default location. An absent file is silently skipped. Set env var `SOLR_SECURITY_AGENT_EXTRA_POLICY` to have the startup script pass this as a `-D` system property. + +|solr.security.agent.mode|SOLR_SECURITY_AGENT_MODE|`warn`|Enforcement mode for the Solr security agent: `warn` (log violations, continue) or `enforce` (log violations, block the operation with `SecurityException`). Set env var `SOLR_SECURITY_AGENT_MODE` to have the startup script pass this as a `-D` system property. + +|SOLR_SECURITY_AGENT_SKIP|_(startup-script only, no system property)_|`false`|If set to `true`, omits the `-javaagent:` flag from the JVM command line, disabling all Solr security agent controls. Intended for temporary troubleshooting only. + |solr.security.auth.basicauth.credentials|basicauth||Defines basic authentication credentials. |solr.security.allow.paths|solr.allowPaths||A comma seperated list of paths for reading from. diff --git a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc index 04dfeb766436..52acaa4044e6 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc @@ -73,6 +73,7 @@ *** xref:jwt-authentication-plugin.adoc[] *** xref:cert-authentication-plugin.adoc[] *** xref:rule-based-authorization-plugin.adoc[] +** xref:security-agent.adoc[] ** xref:audit-logging.adoc[] ** xref:enabling-ssl.adoc[] ** xref:zookeeper-access-control.adoc[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/securing-solr.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/securing-solr.adoc index 7844f073737e..799a9d8697d6 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/securing-solr.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/securing-solr.adoc @@ -1,5 +1,6 @@ = Securing Solr :page-children: authentication-and-authorization-plugins, \ + security-agent, \ audit-logging, \ enabling-ssl, \ zookeeper-access-control, \ @@ -90,6 +91,13 @@ SOLR_IP_ALLOWLIST="127.0.0.1, [::1], 192.168.0.0/24, [2000:123:4:5::]/64" SOLR_IP_DENYLIST="192.168.0.3, 192.168.0.4" ---- +== Security Agent + +Solr includes a Java agent that enforces runtime security controls — file access, outbound network connections, `System.exit()`, and process spawning — at the JVM level. +The agent activates automatically in "warn-only" mode to allow safe observation before enabling blocking enforcement. + +See xref:security-agent.adoc[] for details on enforcement modes, policy configuration, and diagnosing violations. + == Securing ZooKeeper Traffic ZooKeeper is a central and important part of a SolrCloud cluster and understanding how to secure diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc new file mode 100644 index 000000000000..ff9f3e0c2830 --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc @@ -0,0 +1,168 @@ += Security Agent +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +Solr includes a Java agent (`solr-agent-sm`) that aims to fill the gaps left by the removal of Java Security Manager (JSM) in JDK 24. +The agent intercepts file access, network connections, `System.exit()`, and process spawning at the bytecode level, enforcing a configurable policy. + +== Protections Active by Default + +When the agent JAR is present, the startup scripts automatically activate it. +The following protections apply to all code running inside the Solr JVM, including plugins: + +* **File access** — reads and writes are restricted to permitted directories (Solr home, data dir, log dir, temp dir). +* **Outbound network** — connections are restricted to loopback addresses and intra-cluster ports. +* **`System.exit()` / `Runtime.halt()`** — only approved caller classes may shut down the JVM. +* **Process spawning** — `ProcessBuilder.start()` and `Runtime.exec()` are blocked by default. + +== Enforcement Modes + +The agent operates in two modes, controlled by the `SOLR_SECURITY_AGENT_MODE` environment variable: + +`warn` (default):: +Violations are logged at `WARN` level and counted in `/admin/metrics`, but the operation is allowed to continue. +This is the safe default for existing deployments — operators can observe violations without risk. + +`enforce`:: +Violations throw a `SecurityException`, blocking the operation. +Enable this mode once you have verified that your deployment produces no unexpected violations in `warn` mode. + +Set the mode in `solr.in.sh` or `solr.in.cmd`: + +[source,bash] +---- +SOLR_SECURITY_AGENT_MODE=enforce +---- + +The startup script converts this to the JVM system property `-Dsolr.security.agent.mode=enforce`. + +== Policy File Format + +Solr uses JDK-style `.policy` files with Solr-specific variable substitution. +Two files are loaded: + +`server/etc/agent-security.policy` (default):: +The bundled production policy. +Do not edit this file — it will be overwritten on upgrade. + +`server/etc/agent-security-extra.policy` (operator extension):: +Add your custom rules here. +This file is silently skipped if absent. +Its location can be overridden via `SOLR_SECURITY_AGENT_EXTRA_POLICY`. + +=== Variable Substitution + +The following variables are expanded in policy entries: + +[cols="1,2",options="header"] +|=== +|Variable|Resolved Value +|`${solr.home}`|Solr home directory +|`${solr.data.dir}`|Solr data directory +|`${solr.log.dir}`|Solr log directory +|`${solr.install.dir}`|Solr installation root (parent of `server/`) +|`${java.io.tmpdir}`|JVM temporary directory +|`${java.home}`|JDK installation directory +|`${solr.port}`|Solr HTTP port +|`${solr.zk.port}`|ZooKeeper port (defaults to `solr.port + 1000` for embedded ZK) +|=== + +=== Default Intra-Cluster Network Policy + +The bundled policy permits connections on `*:${solr.port}` and `*:${solr.zk.port}` from any host, so all cluster nodes can communicate without any operator action. +If your ZooKeeper runs on a non-standard port, set `-Dsolr.zk.port=` at startup or configure it explicitly in the policy. + +=== Adding Custom Policy Entries + +To permit additional paths or endpoints, add entries to `agent-security-extra.policy`: + +[source,text] +---- +grant { + // permit a plugin that reads from an NFS mount + permission java.io.FilePermission "/mnt/nfs-data/-", "read"; +}; + +grant { + // permit outbound connection to an internal analytics service + permission java.net.SocketPermission "analytics.internal:443", "connect,resolve"; +}; +---- + +To override the extra-policy file location (useful for read-only installations or containers): + +[source,bash] +---- +SOLR_SECURITY_AGENT_EXTRA_POLICY=/etc/solr/custom-security.policy +---- + +== Diagnosing Violations + +Violations appear in the Solr log in this format: + +[source,text] +---- +SECURITY VIOLATION [FILE] target=/etc/passwd caller=com.example.Plugin mode=warn source=DEFAULT +---- + +Fields: + +* `[TYPE]` — `FILE`, `NETWORK`, `EXIT`, or `EXEC` +* `target` — the resource that was accessed or attempted +* `caller` — the class that triggered the access +* `mode` — `warn` or `enforce` +* `source` — `DEFAULT` (bundled policy) or `OPERATOR` (extra policy) + +Violation counts are also available in `/admin/metrics` under: + +* `security.agent.violations.file` +* `security.agent.violations.network` +* `security.agent.violations.exit` +* `security.agent.violations.exec` + +== Disabling the Agent + +To temporarily disable the agent for troubleshooting, set in `solr.in.sh` or `solr.in.cmd`: + +[source,bash] +---- +SOLR_SECURITY_AGENT_SKIP=true +---- + +[WARNING] +==== +Disabling the agent removes all runtime security controls. +Use this only for temporary troubleshooting and re-enable it as soon as possible. +==== + +== Modules with Pre-Permitted Network Access + +The following Solr modules have wildcard outbound network access pre-permitted in the bundled policy: + +* `jwt-auth` +* `opentelemetry` +* `s3-repository` +* `gcs-repository` +* `cross-dc-manager` + +Other modules (such as `extraction`) require explicit policy entries when connecting to external services. +See the module's documentation for details. + +== See Also + +* xref:configuration-guide:solr-properties.adoc[] — `solr.security.agent.*` property reference +* xref:indexing-guide:indexing-with-tika.adoc[] — extraction module network policy requirements diff --git a/solr/solr-ref-guide/modules/indexing-guide/pages/indexing-with-tika.adoc b/solr/solr-ref-guide/modules/indexing-guide/pages/indexing-with-tika.adoc index 1a50f8f14c6e..f7d29965b4b9 100644 --- a/solr/solr-ref-guide/modules/indexing-guide/pages/indexing-with-tika.adoc +++ b/solr/solr-ref-guide/modules/indexing-guide/pages/indexing-with-tika.adoc @@ -72,6 +72,25 @@ Example handler configuration: ---- +[WARNING] +==== +*Security Agent Policy Required for Remote Tika Server* + +When Solr runs with the security agent in `enforce` mode, outbound network connections are blocked by default unless explicitly permitted. Because Solr connects to an external Tika Server over the network, you must add a policy entry to `agent-security-extra.policy` to permit that connection: + +[source,text] +---- +grant { + // extraction module: permit outbound connection to remote Tika Server + permission java.net.SocketPermission "tika-server.internal:9998", "connect,resolve"; +}; +---- + +Replace `tika-server.internal:9998` with the actual hostname and port of your Tika Server. Use a specific `hostname:port`, not a wildcard, to prevent SSRF attacks. + +In the default `warn` mode, the connection will succeed but a `SECURITY VIOLATION` entry will appear in the Solr log. See xref:deployment-guide:security-agent.adoc[Security Agent] for details. +==== + ==== Starting Tika Server with Docker The quickest way to run Tika Server for development is using Docker. The examples below expose Tika on port 9998 on localhost for convenience, matching the handler configuration above. diff --git a/solr/solrj-zookeeper/gradle.lockfile b/solr/solrj-zookeeper/gradle.lockfile index f0f9c46d494c..5b00f6a1c114 100644 --- a/solr/solrj-zookeeper/gradle.lockfile +++ b/solr/solrj-zookeeper/gradle.lockfile @@ -1,17 +1,17 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. -com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.4=jarValidation,testCompileClasspath,testRuntimeClasspath -com.carrotsearch:hppc:0.10.0=jarValidation,testRuntimeClasspath -com.fasterxml.jackson.core:jackson-annotations:2.21=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.fasterxml.jackson.core:jackson-core:2.21.2=jarValidation,runtimeClasspath,testRuntimeClasspath -com.fasterxml.jackson.core:jackson-databind:2.21.2=jarValidation,runtimeClasspath,testRuntimeClasspath -com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.21.2=jarValidation,testRuntimeClasspath -com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.21.2=jarValidation,testRuntimeClasspath -com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.21.2=jarValidation,testRuntimeClasspath -com.fasterxml.jackson:jackson-bom:2.21.2=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.fasterxml.woodstox:woodstox-core:7.0.0=jarValidation,testRuntimeClasspath -com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,errorprone,jarValidation,testAnnotationProcessor,testRuntimeClasspath +com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.4=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.carrotsearch:hppc:0.10.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.core:jackson-annotations:2.21=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.core:jackson-core:2.21.2=jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.core:jackson-databind:2.21.2=jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.21.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.21.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.21.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson:jackson-bom:2.21.2=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.woodstox:woodstox-core:7.0.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,errorprone,jarValidation,testAnnotationProcessor,testRuntimeClasspath,testRuntimeClasspathCopy com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,errorprone,testAnnotationProcessor com.github.spotbugs:spotbugs-annotations:4.9.8=compileClasspath,compileOnlyHelper com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,errorprone,testAnnotationProcessor @@ -19,166 +19,166 @@ com.google.auto.value:auto-value-annotations:1.11.0=annotationProcessor,errorpro com.google.auto:auto-common:1.2.2=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_annotation:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_annotations:2.18.0=apiHelper -com.google.errorprone:error_prone_annotations:2.41.0=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.41.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy com.google.errorprone:error_prone_annotations:2.43.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_check_api:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_core:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.googlejavaformat:google-java-format:1.27.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.guava:failureaccess:1.0.1=apiHelper -com.google.guava:failureaccess:1.0.3=annotationProcessor,compileClasspath,errorprone,jarValidation,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.3=annotationProcessor,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy com.google.guava:guava:32.0.0-jre=apiHelper -com.google.guava:guava:33.5.0-jre=annotationProcessor,compileClasspath,errorprone,jarValidation,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath -com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,apiHelper,compileClasspath,errorprone,jarValidation,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava:33.5.0-jre=annotationProcessor,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,apiHelper,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy com.google.j2objc:j2objc-annotations:2.8=apiHelper -com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,compileClasspath,errorprone,jarValidation,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy com.google.protobuf:protobuf-java:3.25.8=annotationProcessor,errorprone,testAnnotationProcessor -com.j256.simplemagic:simplemagic:1.17=jarValidation,testRuntimeClasspath -com.jayway.jsonpath:json-path:2.9.0=jarValidation,testRuntimeClasspath -com.tdunning:t-digest:3.3=jarValidation,testRuntimeClasspath -commons-cli:commons-cli:1.11.0=jarValidation,testRuntimeClasspath -commons-codec:commons-codec:1.21.0=jarValidation,testRuntimeClasspath +com.j256.simplemagic:simplemagic:1.17=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +com.jayway.jsonpath:json-path:2.9.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +com.tdunning:t-digest:3.3=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +commons-cli:commons-cli:1.11.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +commons-codec:commons-codec:1.21.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy commons-io:commons-io:2.17.0=apiHelper -commons-io:commons-io:2.21.0=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.dropwizard.metrics:metrics-core:3.2.5=jarValidation,testRuntimeClasspath +commons-io:commons-io:2.21.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.dropwizard.metrics:metrics-core:3.2.5=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,errorprone,testAnnotationProcessor io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,errorprone,testAnnotationProcessor io.netty:netty-buffer:4.1.119.Final=apiHelper -io.netty:netty-buffer:4.2.12.Final=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-codec-base:4.2.12.Final=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-buffer:4.2.12.Final=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-codec-base:4.2.12.Final=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.netty:netty-codec:4.1.119.Final=apiHelper io.netty:netty-common:4.1.119.Final=apiHelper -io.netty:netty-common:4.2.12.Final=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-common:4.2.12.Final=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.netty:netty-handler:4.1.119.Final=apiHelper -io.netty:netty-handler:4.2.12.Final=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-handler:4.2.12.Final=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.netty:netty-resolver:4.1.119.Final=apiHelper -io.netty:netty-resolver:4.2.12.Final=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-resolver:4.2.12.Final=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.netty:netty-tcnative-boringssl-static:2.0.70.Final=apiHelper -io.netty:netty-tcnative-boringssl-static:2.0.75.Final=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-tcnative-boringssl-static:2.0.75.Final=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.netty:netty-tcnative-classes:2.0.70.Final=apiHelper -io.netty:netty-tcnative-classes:2.0.75.Final=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-tcnative-classes:2.0.75.Final=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.netty:netty-transport-classes-epoll:4.1.119.Final=apiHelper -io.netty:netty-transport-classes-epoll:4.2.12.Final=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport-classes-epoll:4.2.12.Final=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.netty:netty-transport-native-epoll:4.1.119.Final=apiHelper -io.netty:netty-transport-native-epoll:4.2.12.Final=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport-native-epoll:4.2.12.Final=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.netty:netty-transport-native-unix-common:4.1.119.Final=apiHelper -io.netty:netty-transport-native-unix-common:4.2.12.Final=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport-native-unix-common:4.2.12.Final=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.netty:netty-transport:4.1.119.Final=apiHelper -io.netty:netty-transport:4.2.12.Final=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator:2.22.0-alpha=jarValidation,testRuntimeClasspath -io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:2.22.0=jarValidation,testRuntimeClasspath -io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java17:2.22.0-alpha=jarValidation,testRuntimeClasspath -io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java8:2.22.0-alpha=jarValidation,testRuntimeClasspath -io.opentelemetry.semconv:opentelemetry-semconv:1.37.0=jarValidation,testRuntimeClasspath -io.opentelemetry:opentelemetry-api-incubator:1.56.0-alpha=jarValidation,testRuntimeClasspath -io.opentelemetry:opentelemetry-api:1.56.0=jarValidation,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-common:1.56.0=jarValidation,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-context:1.56.0=jarValidation,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-exporter-prometheus:1.56.0-alpha=jarValidation,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-common:1.56.0=jarValidation,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-metrics:1.56.0=jarValidation,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-trace:1.56.0=jarValidation,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk:1.56.0=jarValidation,testRuntimeClasspath -io.prometheus:prometheus-metrics-exposition-formats:1.1.0=jarValidation,testRuntimeClasspath -io.prometheus:prometheus-metrics-model:1.1.0=jarValidation,testRuntimeClasspath -io.sgr:s2-geometry-library-java:1.0.0=jarValidation,testRuntimeClasspath -io.swagger.core.v3:swagger-annotations-jakarta:2.2.22=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -jakarta.activation:jakarta.activation-api:2.1.3=jarValidation,testRuntimeClasspath -jakarta.annotation:jakarta.annotation-api:3.0.0=jarValidation,testRuntimeClasspath -jakarta.inject:jakarta.inject-api:2.0.1=jarValidation,testRuntimeClasspath -jakarta.servlet:jakarta.servlet-api:6.1.0=jarValidation,testRuntimeClasspath -jakarta.validation:jakarta.validation-api:3.1.0=jarValidation,testRuntimeClasspath -jakarta.ws.rs:jakarta.ws.rs-api:3.1.0=jarValidation,runtimeClasspath -jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=testRuntimeClasspath -jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jarValidation,testRuntimeClasspath +io.netty:netty-transport:4.2.12.Final=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator:2.22.0-alpha=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:2.22.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java17:2.22.0-alpha=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java8:2.22.0-alpha=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.semconv:opentelemetry-semconv:1.37.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-api-incubator:1.56.0-alpha=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-api:1.56.0=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-common:1.56.0=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-context:1.56.0=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-exporter-prometheus:1.56.0-alpha=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk-common:1.56.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk-metrics:1.56.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk-trace:1.56.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk:1.56.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.prometheus:prometheus-metrics-exposition-formats:1.1.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.prometheus:prometheus-metrics-model:1.1.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.sgr:s2-geometry-library-java:1.0.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.swagger.core.v3:swagger-annotations-jakarta:2.2.22=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.activation:jakarta.activation-api:2.1.3=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.annotation:jakarta.annotation-api:3.0.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.inject:jakarta.inject-api:2.0.1=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.servlet:jakarta.servlet-api:6.1.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.validation:jakarta.validation-api:3.1.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.ws.rs:jakarta.ws.rs-api:3.1.0=jarValidation,runtimeClasspath,runtimeClasspathCopy +jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor -junit:junit:4.13.2=jarValidation,testCompileClasspath,testRuntimeClasspath -org.antlr:antlr4-runtime:4.13.2=jarValidation,testRuntimeClasspath -org.apache.commons:commons-exec:1.6.0=jarValidation,testRuntimeClasspath -org.apache.commons:commons-lang3:3.20.0=jarValidation,testRuntimeClasspath -org.apache.commons:commons-math3:3.6.1=jarValidation,testRuntimeClasspath -org.apache.curator:curator-client:5.9.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.curator:curator-framework:5.9.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.curator:curator-test:5.9.0=jarValidation,testRuntimeClasspath -org.apache.httpcomponents:httpclient:4.5.14=jarValidation,testRuntimeClasspath -org.apache.httpcomponents:httpcore:4.4.16=jarValidation,testRuntimeClasspath -org.apache.httpcomponents:httpmime:4.5.14=jarValidation,testRuntimeClasspath -org.apache.logging.log4j:log4j-api:2.25.3=jarValidation,testRuntimeClasspath -org.apache.logging.log4j:log4j-core:2.25.3=jarValidation,testRuntimeClasspath -org.apache.logging.log4j:log4j-slf4j2-impl:2.25.3=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-analysis-common:10.4.0=jarValidation,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-analysis-kuromoji:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-analysis-nori:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-analysis-phonetic:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-backward-codecs:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-classification:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-codecs:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-core:10.4.0=jarValidation,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-expressions:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-facet:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-grouping:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-highlighter:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-join:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-memory:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-misc:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-queries:10.4.0=jarValidation,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-queryparser:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-sandbox:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-spatial-extras:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-spatial3d:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-suggest:10.4.0=jarValidation,testRuntimeClasspath -org.apache.lucene:lucene-test-framework:10.4.0=jarValidation,testCompileClasspath,testRuntimeClasspath -org.apache.zookeeper:zookeeper-jute:3.9.4=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.zookeeper:zookeeper:3.9.4=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apiguardian:apiguardian-api:1.1.2=jarValidation,testRuntimeClasspath +junit:junit:4.13.2=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.antlr:antlr4-runtime:4.13.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.commons:commons-exec:1.6.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.commons:commons-lang3:3.20.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.commons:commons-math3:3.6.1=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.curator:curator-client:5.9.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.curator:curator-framework:5.9.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.curator:curator-test:5.9.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.httpcomponents:httpclient:4.5.14=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.httpcomponents:httpcore:4.4.16=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.httpcomponents:httpmime:4.5.14=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.logging.log4j:log4j-api:2.25.3=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.logging.log4j:log4j-core:2.25.3=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.logging.log4j:log4j-slf4j2-impl:2.25.3=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-common:10.4.0=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-kuromoji:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-nori:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-phonetic:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-backward-codecs:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-classification:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-codecs:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-core:10.4.0=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-expressions:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-facet:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-grouping:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-highlighter:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-join:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-memory:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-misc:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-queries:10.4.0=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-queryparser:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-sandbox:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-spatial-extras:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-spatial3d:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-suggest:10.4.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-test-framework:10.4.0=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.zookeeper:zookeeper-jute:3.9.4=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.zookeeper:zookeeper:3.9.4=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apiguardian:apiguardian-api:1.1.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy org.checkerframework:checker-qual:3.33.0=apiHelper -org.codehaus.woodstox:stax2-api:4.2.2=jarValidation,testRuntimeClasspath -org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.34=jarValidation,testRuntimeClasspath -org.eclipse.jetty.http2:jetty-http2-client-transport:12.0.34=jarValidation,testRuntimeClasspath -org.eclipse.jetty.http2:jetty-http2-client:12.0.34=jarValidation,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:jetty-http2-common:12.0.34=jarValidation,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:jetty-http2-hpack:12.0.34=jarValidation,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:jetty-http2-server:12.0.34=jarValidation,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-client:12.0.34=jarValidation,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-java-client:12.0.34=jarValidation,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-java-server:12.0.34=jarValidation,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-server:12.0.34=jarValidation,testRuntimeClasspath -org.eclipse.jetty:jetty-client:12.0.34=jarValidation,testRuntimeClasspath -org.eclipse.jetty:jetty-http:12.0.34=jarValidation,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-io:12.0.34=jarValidation,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-rewrite:12.0.34=jarValidation,testRuntimeClasspath -org.eclipse.jetty:jetty-security:12.0.34=jarValidation,testRuntimeClasspath -org.eclipse.jetty:jetty-server:12.0.34=jarValidation,testRuntimeClasspath -org.eclipse.jetty:jetty-session:12.0.34=jarValidation,testRuntimeClasspath -org.eclipse.jetty:jetty-util:12.0.34=jarValidation,testCompileClasspath,testRuntimeClasspath -org.glassfish.hk2.external:aopalliance-repackaged:4.0.0-M3=jarValidation,testRuntimeClasspath -org.glassfish.hk2:hk2-api:4.0.0-M3=jarValidation,testRuntimeClasspath -org.glassfish.hk2:hk2-locator:4.0.0-M3=jarValidation,testRuntimeClasspath -org.glassfish.hk2:hk2-utils:4.0.0-M3=jarValidation,testRuntimeClasspath -org.glassfish.hk2:osgi-resource-locator:3.0.0=jarValidation,testRuntimeClasspath -org.glassfish.jersey.containers:jersey-container-jetty-http:4.0.2=jarValidation,testRuntimeClasspath -org.glassfish.jersey.core:jersey-client:4.0.2=jarValidation,testRuntimeClasspath -org.glassfish.jersey.core:jersey-common:4.0.2=jarValidation,testRuntimeClasspath -org.glassfish.jersey.core:jersey-server:4.0.2=jarValidation,testRuntimeClasspath -org.glassfish.jersey.ext:jersey-entity-filtering:4.0.2=jarValidation,testRuntimeClasspath -org.glassfish.jersey.inject:jersey-hk2:4.0.2=jarValidation,testRuntimeClasspath -org.glassfish.jersey.media:jersey-media-json-jackson:4.0.2=jarValidation,testRuntimeClasspath -org.glassfish.jersey:jersey-bom:4.0.2=jarValidation,testRuntimeClasspath -org.hamcrest:hamcrest:3.0=jarValidation,testCompileClasspath,testRuntimeClasspath -org.javassist:javassist:3.30.2-GA=jarValidation,testRuntimeClasspath -org.jspecify:jspecify:1.0.0=annotationProcessor,compileClasspath,errorprone,jarValidation,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.6.2=jarValidation,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.6.2=jarValidation,testRuntimeClasspath +org.codehaus.woodstox:stax2-api:4.2.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-client-transport:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-client:12.0.34=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-common:12.0.34=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-hpack:12.0.34=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-server:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-client:12.0.34=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-java-client:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-java-server:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-server:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-client:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-http:12.0.34=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-io:12.0.34=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-rewrite:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-security:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-server:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-session:12.0.34=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-util:12.0.34=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2.external:aopalliance-repackaged:4.0.0-M3=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2:hk2-api:4.0.0-M3=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2:hk2-locator:4.0.0-M3=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2:hk2-utils:4.0.0-M3=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2:osgi-resource-locator:3.0.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.containers:jersey-container-jetty-http:4.0.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.core:jersey-client:4.0.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.core:jersey-common:4.0.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.core:jersey-server:4.0.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.ext:jersey-entity-filtering:4.0.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.inject:jersey-hk2:4.0.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.media:jersey-media-json-jackson:4.0.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey:jersey-bom:4.0.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.hamcrest:hamcrest:3.0=jarValidation,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.javassist:javassist:3.30.2-GA=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.jspecify:jspecify:1.0.0=annotationProcessor,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.junit.jupiter:junit-jupiter-api:5.6.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.junit.platform:junit-platform-commons:1.6.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy org.junit:junit-bom:5.14.0=compileOnlyHelper -org.junit:junit-bom:5.6.2=jarValidation,testRuntimeClasspath -org.locationtech.spatial4j:spatial4j:0.8=jarValidation,testRuntimeClasspath -org.opentest4j:opentest4j:1.2.0=jarValidation,testRuntimeClasspath -org.ow2.asm:asm-commons:9.8=jarValidation,testRuntimeClasspath -org.ow2.asm:asm-tree:9.8=jarValidation,testRuntimeClasspath -org.ow2.asm:asm:9.8=jarValidation,testRuntimeClasspath +org.junit:junit-bom:5.6.2=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.locationtech.spatial4j:spatial4j:0.8=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.opentest4j:opentest4j:1.2.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.ow2.asm:asm-commons:9.8=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.ow2.asm:asm-tree:9.8=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +org.ow2.asm:asm:9.8=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy org.pcollections:pcollections:4.0.1=annotationProcessor,errorprone,testAnnotationProcessor -org.semver4j:semver4j:6.0.0=jarValidation,runtimeClasspath,testRuntimeClasspath -org.slf4j:jcl-over-slf4j:2.0.17=jarValidation,runtimeClasspath,testRuntimeClasspath +org.semver4j:semver4j:6.0.0=jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.slf4j:jcl-over-slf4j:2.0.17=jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy org.slf4j:slf4j-api:2.0.13=apiHelper -org.slf4j:slf4j-api:2.0.17=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.xerial.snappy:snappy-java:1.1.10.8=jarValidation,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.17=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.xerial.snappy:snappy-java:1.1.10.8=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy empty=apiHelperTest,compileOnlyHelperTest,missingdoclet,permitAggregatorUse,permitTestAggregatorUse,permitTestUnusedDeclared,permitTestUsedUndeclared,permitUnusedDeclared,permitUsedUndeclared,signatures diff --git a/solr/solrj/gradle.lockfile b/solr/solrj/gradle.lockfile index 449d53a460d7..579f2ec25f96 100644 --- a/solr/solrj/gradle.lockfile +++ b/solr/solrj/gradle.lockfile @@ -17,13 +17,19 @@ com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,compileOnlyHelp com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,errorprone,testAnnotationProcessor com.google.auto.value:auto-value-annotations:1.11.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.auto:auto-common:1.2.2=annotationProcessor,errorprone,testAnnotationProcessor +com.google.code.findbugs:jsr305:3.0.2=spotless865458226 com.google.errorprone:error_prone_annotation:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor +com.google.errorprone:error_prone_annotations:2.18.0=spotless865458226 com.google.errorprone:error_prone_annotations:2.41.0=jarValidation,testCompileClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.43.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_check_api:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_core:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor +com.google.googlejavaformat:google-java-format:1.18.1=spotless865458226 com.google.googlejavaformat:google-java-format:1.27.0=annotationProcessor,errorprone,testAnnotationProcessor +com.google.guava:failureaccess:1.0.1=spotless865458226 com.google.guava:failureaccess:1.0.3=annotationProcessor,errorprone,jarValidation,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +com.google.guava:guava-parent:32.1.1-jre=spotless865458226 +com.google.guava:guava:32.1.1-jre=spotless865458226 com.google.guava:guava:33.5.0-jre=annotationProcessor,errorprone,jarValidation,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,errorprone,jarValidation,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,errorprone,jarValidation,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath @@ -115,6 +121,7 @@ org.apache.lucene:lucene-test-framework:10.4.0=jarValidation,testCompileClasspat org.apache.zookeeper:zookeeper-jute:3.9.4=jarValidation,testCompileClasspath,testRuntimeClasspath org.apache.zookeeper:zookeeper:3.9.4=jarValidation,testCompileClasspath,testRuntimeClasspath org.apiguardian:apiguardian-api:1.1.2=jarValidation,testRuntimeClasspath +org.checkerframework:checker-qual:3.33.0=spotless865458226 org.codehaus.woodstox:stax2-api:4.2.2=jarValidation,testRuntimeClasspath org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.34=jarValidation,testCompileClasspath,testRuntimeClasspath org.eclipse.jetty.ee10:jetty-ee10-webapp:12.0.34=jarValidation,testCompileClasspath,testRuntimeClasspath diff --git a/solr/test-framework/gradle.lockfile b/solr/test-framework/gradle.lockfile index e55bbe89b7fb..b658b8c1143e 100644 --- a/solr/test-framework/gradle.lockfile +++ b/solr/test-framework/gradle.lockfile @@ -1,170 +1,170 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. -biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,testCompileClasspath -com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.4=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.carrotsearch:hppc:0.10.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -com.fasterxml.jackson.core:jackson-annotations:2.21=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.fasterxml.jackson.core:jackson-core:2.21.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -com.fasterxml.jackson.core:jackson-databind:2.21.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.21.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.21.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.21.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -com.fasterxml.jackson:jackson-bom:2.21.2=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.fasterxml.woodstox:woodstox-core:7.0.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,apiHelper,errorprone,jarValidation,runtimeClasspath,testAnnotationProcessor,testRuntimeClasspath +biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,compileClasspathCopy,testCompileClasspath,testCompileClasspathCopy +com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.4=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.carrotsearch:hppc:0.10.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.core:jackson-annotations:2.21=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.core:jackson-core:2.21.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.core:jackson-databind:2.21.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.21.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.21.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.21.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.jackson:jackson-bom:2.21.2=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.fasterxml.woodstox:woodstox-core:7.0.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,apiHelper,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,testAnnotationProcessor,testRuntimeClasspath,testRuntimeClasspathCopy com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,errorprone,testAnnotationProcessor com.github.spotbugs:spotbugs-annotations:4.9.8=compileClasspath,compileOnlyHelper com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,errorprone,testAnnotationProcessor com.google.auto.value:auto-value-annotations:1.11.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.auto:auto-common:1.2.2=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_annotation:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor -com.google.errorprone:error_prone_annotations:2.41.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.google.errorprone:error_prone_annotations:2.41.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy com.google.errorprone:error_prone_annotations:2.43.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_check_api:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.errorprone:error_prone_core:2.41.0=annotationProcessor,errorprone,testAnnotationProcessor com.google.googlejavaformat:google-java-format:1.27.0=annotationProcessor,errorprone,testAnnotationProcessor -com.google.guava:failureaccess:1.0.3=annotationProcessor,apiHelper,compileClasspath,errorprone,jarValidation,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath -com.google.guava:guava:33.5.0-jre=annotationProcessor,apiHelper,compileClasspath,errorprone,jarValidation,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath -com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,apiHelper,compileClasspath,errorprone,jarValidation,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath -com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,apiHelper,compileClasspath,errorprone,jarValidation,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath +com.google.guava:failureaccess:1.0.3=annotationProcessor,apiHelper,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.google.guava:guava:33.5.0-jre=annotationProcessor,apiHelper,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,apiHelper,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,apiHelper,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy com.google.protobuf:protobuf-java:3.25.8=annotationProcessor,errorprone,testAnnotationProcessor -com.j256.simplemagic:simplemagic:1.17=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -com.jayway.jsonpath:json-path:2.9.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -com.tdunning:t-digest:3.3=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -commons-cli:commons-cli:1.11.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-codec:commons-codec:1.21.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-io:commons-io:2.21.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.dropwizard.metrics:metrics-core:3.2.5=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.j256.simplemagic:simplemagic:1.17=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.jayway.jsonpath:json-path:2.9.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +com.tdunning:t-digest:3.3=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +commons-cli:commons-cli:1.11.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +commons-codec:commons-codec:1.21.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +commons-io:commons-io:2.21.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.dropwizard.metrics:metrics-core:3.2.5=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,errorprone,testAnnotationProcessor io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,errorprone,testAnnotationProcessor -io.netty:netty-buffer:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-codec-base:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-common:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-handler:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-resolver:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-tcnative-boringssl-static:2.0.75.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-tcnative-classes:2.0.75.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-transport-classes-epoll:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-transport-native-epoll:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-transport-native-unix-common:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.netty:netty-transport:4.2.12.Final=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator:2.22.0-alpha=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:2.22.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java17:2.22.0-alpha=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java8:2.22.0-alpha=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -io.opentelemetry.semconv:opentelemetry-semconv:1.37.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-api-incubator:1.56.0-alpha=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-api:1.56.0=apiHelper,compileClasspath,jarValidation,permitUnusedDeclared,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-common:1.56.0=apiHelper,compileClasspath,jarValidation,permitUnusedDeclared,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-context:1.56.0=apiHelper,compileClasspath,jarValidation,permitUnusedDeclared,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-exporter-prometheus:1.56.0-alpha=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-common:1.56.0=apiHelper,compileClasspath,jarValidation,permitUnusedDeclared,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-metrics:1.56.0=apiHelper,compileClasspath,jarValidation,permitUnusedDeclared,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-trace:1.56.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk:1.56.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -io.prometheus:prometheus-metrics-exposition-formats:1.1.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.prometheus:prometheus-metrics-model:1.1.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.sgr:s2-geometry-library-java:1.0.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -io.swagger.core.v3:swagger-annotations-jakarta:2.2.22=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -jakarta.activation:jakarta.activation-api:2.1.3=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -jakarta.annotation:jakarta.annotation-api:3.0.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -jakarta.inject:jakarta.inject-api:2.0.1=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -jakarta.servlet:jakarta.servlet-api:6.1.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -jakarta.validation:jakarta.validation-api:3.1.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath +io.netty:netty-buffer:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-codec-base:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-common:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-handler:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-resolver:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-tcnative-boringssl-static:2.0.75.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-tcnative-classes:2.0.75.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-transport-classes-epoll:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-transport-native-epoll:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-transport-native-unix-common:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.netty:netty-transport:4.2.12.Final=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator:2.22.0-alpha=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:2.22.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java17:2.22.0-alpha=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java8:2.22.0-alpha=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry.semconv:opentelemetry-semconv:1.37.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-api-incubator:1.56.0-alpha=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-api:1.56.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,permitUnusedDeclared,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-common:1.56.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,permitUnusedDeclared,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-context:1.56.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,permitUnusedDeclared,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-exporter-prometheus:1.56.0-alpha=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk-common:1.56.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,permitUnusedDeclared,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk-metrics:1.56.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,permitUnusedDeclared,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk-trace:1.56.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.opentelemetry:opentelemetry-sdk:1.56.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.prometheus:prometheus-metrics-exposition-formats:1.1.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.prometheus:prometheus-metrics-model:1.1.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.sgr:s2-geometry-library-java:1.0.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +io.swagger.core.v3:swagger-annotations-jakarta:2.2.22=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.activation:jakarta.activation-api:2.1.3=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.annotation:jakarta.annotation-api:3.0.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.inject:jakarta.inject-api:2.0.1=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.servlet:jakarta.servlet-api:6.1.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.validation:jakarta.validation-api:3.1.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor -junit:junit:4.13.2=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.antlr:antlr4-runtime:4.13.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.commons:commons-exec:1.6.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.commons:commons-lang3:3.20.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.commons:commons-math3:3.6.1=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.curator:curator-client:5.9.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.curator:curator-framework:5.9.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.curator:curator-test:5.9.0=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.httpcomponents:httpclient:4.5.14=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.httpcomponents:httpcore:4.4.16=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.httpcomponents:httpmime:4.5.14=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.logging.log4j:log4j-api:2.25.3=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.logging.log4j:log4j-core:2.25.3=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.logging.log4j:log4j-slf4j2-impl:2.25.3=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-analysis-common:10.4.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-analysis-kuromoji:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-analysis-nori:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-analysis-phonetic:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-backward-codecs:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-classification:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-codecs:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-core:10.4.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-expressions:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-facet:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-grouping:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-highlighter:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-join:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-memory:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-misc:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-queries:10.4.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-queryparser:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-sandbox:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-spatial-extras:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-spatial3d:10.4.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.apache.lucene:lucene-suggest:10.4.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.lucene:lucene-test-framework:10.4.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.zookeeper:zookeeper-jute:3.9.4=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.zookeeper:zookeeper:3.9.4=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apiguardian:apiguardian-api:1.1.2=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.codehaus.woodstox:stax2-api:4.2.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.34=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:jetty-http2-client-transport:12.0.34=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:jetty-http2-client:12.0.34=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:jetty-http2-common:12.0.34=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:jetty-http2-hpack:12.0.34=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.http2:jetty-http2-server:12.0.34=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-client:12.0.34=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-java-client:12.0.34=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-java-server:12.0.34=jarValidation,runtimeClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-alpn-server:12.0.34=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-client:12.0.34=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-http:12.0.34=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-io:12.0.34=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-rewrite:12.0.34=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-security:12.0.34=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-server:12.0.34=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-session:12.0.34=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-util:12.0.34=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.glassfish.hk2.external:aopalliance-repackaged:4.0.0-M3=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.glassfish.hk2:hk2-api:4.0.0-M3=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.glassfish.hk2:hk2-locator:4.0.0-M3=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.glassfish.hk2:hk2-utils:4.0.0-M3=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.glassfish.hk2:osgi-resource-locator:3.0.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.glassfish.jersey.containers:jersey-container-jetty-http:4.0.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.glassfish.jersey.core:jersey-client:4.0.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.glassfish.jersey.core:jersey-common:4.0.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.glassfish.jersey.core:jersey-server:4.0.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.glassfish.jersey.ext:jersey-entity-filtering:4.0.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.glassfish.jersey.inject:jersey-hk2:4.0.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.glassfish.jersey.media:jersey-media-json-jackson:4.0.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.glassfish.jersey:jersey-bom:4.0.2=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:3.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.javassist:javassist:3.30.2-GA=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.jspecify:jspecify:1.0.0=annotationProcessor,apiHelper,compileClasspath,errorprone,jarValidation,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.6.2=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.6.2=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +junit:junit:4.13.2=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.antlr:antlr4-runtime:4.13.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.commons:commons-exec:1.6.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.commons:commons-lang3:3.20.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.commons:commons-math3:3.6.1=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.curator:curator-client:5.9.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.curator:curator-framework:5.9.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.curator:curator-test:5.9.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.httpcomponents:httpclient:4.5.14=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.httpcomponents:httpcore:4.4.16=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.httpcomponents:httpmime:4.5.14=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.logging.log4j:log4j-api:2.25.3=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.logging.log4j:log4j-core:2.25.3=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.logging.log4j:log4j-slf4j2-impl:2.25.3=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-common:10.4.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-kuromoji:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-nori:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-analysis-phonetic:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-backward-codecs:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-classification:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-codecs:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-core:10.4.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-expressions:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-facet:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-grouping:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-highlighter:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-join:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-memory:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-misc:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-queries:10.4.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-queryparser:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-sandbox:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-spatial-extras:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-spatial3d:10.4.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-suggest:10.4.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.lucene:lucene-test-framework:10.4.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.zookeeper:zookeeper-jute:3.9.4=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apache.zookeeper:zookeeper:3.9.4=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.apiguardian:apiguardian-api:1.1.2=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.codehaus.woodstox:stax2-api:4.2.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.34=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-client-transport:12.0.34=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-client:12.0.34=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-common:12.0.34=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-hpack:12.0.34=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty.http2:jetty-http2-server:12.0.34=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-client:12.0.34=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-java-client:12.0.34=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-java-server:12.0.34=jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-alpn-server:12.0.34=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-client:12.0.34=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-http:12.0.34=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-io:12.0.34=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-rewrite:12.0.34=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-security:12.0.34=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-server:12.0.34=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-session:12.0.34=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.eclipse.jetty:jetty-util:12.0.34=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2.external:aopalliance-repackaged:4.0.0-M3=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2:hk2-api:4.0.0-M3=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2:hk2-locator:4.0.0-M3=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2:hk2-utils:4.0.0-M3=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.hk2:osgi-resource-locator:3.0.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.containers:jersey-container-jetty-http:4.0.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.core:jersey-client:4.0.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.core:jersey-common:4.0.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.core:jersey-server:4.0.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.ext:jersey-entity-filtering:4.0.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.inject:jersey-hk2:4.0.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey.media:jersey-media-json-jackson:4.0.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.glassfish.jersey:jersey-bom:4.0.2=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.hamcrest:hamcrest:3.0=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.javassist:javassist:3.30.2-GA=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.jspecify:jspecify:1.0.0=annotationProcessor,apiHelper,compileClasspath,compileClasspathCopy,errorprone,jarValidation,runtimeClasspath,runtimeClasspathCopy,testAnnotationProcessor,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.junit.jupiter:junit-jupiter-api:5.6.2=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.junit.platform:junit-platform-commons:1.6.2=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy org.junit:junit-bom:5.14.0=compileOnlyHelper -org.junit:junit-bom:5.6.2=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.locationtech.spatial4j:spatial4j:0.8=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.2.0=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,testCompileClasspath -org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,testCompileClasspath -org.osgi:org.osgi.resource:1.0.0=compileClasspath,testCompileClasspath -org.osgi:org.osgi.service.serviceloader:1.0.0=compileClasspath,testCompileClasspath -org.ow2.asm:asm-commons:9.8=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.ow2.asm:asm-tree:9.8=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.ow2.asm:asm:9.8=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath +org.junit:junit-bom:5.6.2=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.locationtech.spatial4j:spatial4j:0.8=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.opentest4j:opentest4j:1.2.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,compileClasspathCopy,testCompileClasspath,testCompileClasspathCopy +org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,compileClasspathCopy,testCompileClasspath,testCompileClasspathCopy +org.osgi:org.osgi.resource:1.0.0=compileClasspath,compileClasspathCopy,testCompileClasspath,testCompileClasspathCopy +org.osgi:org.osgi.service.serviceloader:1.0.0=compileClasspath,compileClasspathCopy,testCompileClasspath,testCompileClasspathCopy +org.ow2.asm:asm-commons:9.8=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.ow2.asm:asm-tree:9.8=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.ow2.asm:asm:9.8=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy org.pcollections:pcollections:4.0.1=annotationProcessor,errorprone,testAnnotationProcessor -org.semver4j:semver4j:6.0.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.slf4j:jcl-over-slf4j:2.0.17=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -org.slf4j:slf4j-api:2.0.17=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.xerial.snappy:snappy-java:1.1.10.8=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.semver4j:semver4j:6.0.0=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.slf4j:jcl-over-slf4j:2.0.17=apiHelper,jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.slf4j:slf4j-api:2.0.17=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.xerial.snappy:snappy-java:1.1.10.8=apiHelper,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy empty=apiHelperTest,compileOnlyHelperTest,missingdoclet,permitAggregatorUse,permitTestAggregatorUse,permitTestUnusedDeclared,permitTestUsedUndeclared,permitUsedUndeclared,signatures diff --git a/specs/001-jsm-replacement/checklists/requirements.md b/specs/001-jsm-replacement/checklists/requirements.md new file mode 100644 index 000000000000..c6f99f12ab2d --- /dev/null +++ b/specs/001-jsm-replacement/checklists/requirements.md @@ -0,0 +1,36 @@ +# Specification Quality Checklist: Java Security Manager Replacement + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-04-28 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- The spec explicitly calls out that the OpenSearch `agent-sm` module is a *candidate* for evaluation during planning — this is intentional and does not leak implementation details into the spec itself. +- The 5% performance budget in FR-012 / SC-002 is a reasonable default; the planning phase should validate this against actual benchmarks. +- All checklist items pass. Ready to proceed to `/speckit-plan`. diff --git a/specs/001-jsm-replacement/contracts/policy-file-format.md b/specs/001-jsm-replacement/contracts/policy-file-format.md new file mode 100644 index 000000000000..ccbafc7c93cf --- /dev/null +++ b/specs/001-jsm-replacement/contracts/policy-file-format.md @@ -0,0 +1,140 @@ +# Contract: Security Policy File Format + +**Type**: Configuration file format +**Operator-facing**: Yes +**Files**: `${solr.home}/server/etc/agent-security.policy`, `agent-security-extra.policy` + +**Extra policy file path**: Defaults to `${server.dir}/etc/agent-security-extra.policy`. Override via: +- Environment variable: `SOLR_SECURITY_AGENT_EXTRA_POLICY=/path/to/my.policy` +- System property: `-Dsolr.security.agent.extra.policy=/path/to/my.policy` + +An absent file is silently skipped (not a startup error). This override supports read-only install trees, container images, and config-management tooling where the Solr server directory is not writable. Note: `solr.xml` is not a supported source for this setting — the agent initialises before `solr.xml` is parsed. + +--- + +## Overview + +Solr's agent-based security controls are configured via JDK-style `.policy` files, the same syntax used by the historical Java Security Manager. This allows operators familiar with JSM policy files to reuse their knowledge. + +Two files are loaded at startup: +1. **`agent-security.policy`** — Solr's default policy (shipped with Solr, not meant to be edited). +2. **`agent-security-extra.policy`** — Operator extension policy (optional, empty by default). + +--- + +## File Syntax + +Standard JDK policy file syntax: + +``` +// Line comments are supported + +grant [codeBase "url"] { + permission ["target"] [, "actions"]; + ... +}; +``` + +### Supported Permission Types + +| Permission Class | Target | Actions | Effect | +|-----------------|--------|---------|--------| +| `java.io.FilePermission` | Absolute path or `path/-` (recursive) | `"read"`, `"write"`, `"delete"`, `"read,write"` | Permits file operations on matching paths | +| `java.net.SocketPermission` | `"host:port"` | `"connect,resolve"` | Permits outbound connection to host:port | +| `java.lang.RuntimePermission` | `"exitVM"` | _(none)_ | Grants permission to call `System.exit()` | +| `java.lang.RuntimePermission` | `"exec"` | _(none)_ | Grants permission to spawn child processes | + +### Variable Substitution + +The following variables are expanded in path targets: + +| Variable | Resolved Value | +|----------|---------------| +| `${solr.home}` | Solr home directory | +| `${solr.data.dir}` | Solr data directory | +| `${solr.log.dir}` | Solr log directory | +| `${solr.install.dir}` | Solr installation root (parent of `server/`); used in `codeBase` paths for module JARs | +| `${java.io.tmpdir}` | JVM temporary directory | +| `${java.home}` | JDK installation directory | +| `${user.home}` | OS user home directory | + +--- + +## Default Policy (Conceptual) + +The default `agent-security.policy` permits: + +``` +grant { + // Solr home (read-only for config files) + permission java.io.FilePermission "${solr.home}/-", "read"; + + // Solr data and index directories (read + write) + permission java.io.FilePermission "${solr.data.dir}/-", "read,write,delete"; + + // Log directory (write) + permission java.io.FilePermission "${solr.log.dir}/-", "read,write,delete"; + + // Temporary files + permission java.io.FilePermission "${java.io.tmpdir}/-", "read,write,delete"; + + // JDK runtime libraries (read-only) + permission java.io.FilePermission "${java.home}/-", "read"; + + // Loopback network (inter-thread, localhost HTTP) + permission java.net.SocketPermission "localhost:1-65535", "connect,resolve"; + permission java.net.SocketPermission "127.0.0.1:1-65535", "connect,resolve"; + + // ZooKeeper ensemble (populated at startup from cluster config) + // permission java.net.SocketPermission ":", "connect,resolve"; + + // Approved System.exit() callers + permission java.lang.RuntimePermission "exitVM"; + // (codeBase-restricted to Solr CLI and shutdown hooks in full default policy) +}; +``` + +--- + +## Operator Extension Example + +To permit a plugin that reads from `/mnt/nfs-data` and connects to `analytics.internal:443`: + +``` +// ${solr.home}/server/etc/agent-security-extra.policy + +grant { + permission java.io.FilePermission "/mnt/nfs-data/-", "read"; + permission java.net.SocketPermission "analytics.internal:443", "connect,resolve"; +}; +``` + +--- + +## Enforcement Mode + +Controlled via the `SOLR_SECURITY_AGENT_MODE` environment variable (auto-converted by `EnvUtils` to system property `solr.security.agent.mode`): + +| Value | Behaviour | +|-------|-----------| +| `warn` (default) | Violations are logged at WARN; operation proceeds | +| `enforce` | Violations are logged at ERROR; operation is blocked with `SecurityException` | + +Set in `solr.in.sh`: +```bash +SOLR_SECURITY_AGENT_MODE=enforce +``` + +--- + +## Violation Log Format + +When a violation is detected, Solr logs a structured message: + +``` +[WARN ] SecurityAgent - SECURITY VIOLATION [FILE_READ] target=/etc/passwd caller=com.example.BadPlugin mode=WARN + at com.example.BadPlugin.readConfig(BadPlugin.java:42) + at org.apache.solr.core.CoreContainer.loadCore(CoreContainer.java:...) +``` + +Fields always present: operation type, target, top caller class, enforcement mode. diff --git a/specs/001-jsm-replacement/data-model.md b/specs/001-jsm-replacement/data-model.md new file mode 100644 index 000000000000..9a9c56556e90 --- /dev/null +++ b/specs/001-jsm-replacement/data-model.md @@ -0,0 +1,131 @@ +# Data Model: Java Security Manager Replacement + +**Date**: 2026-04-28 +**Feature**: specs/001-jsm-replacement/spec.md + +This feature introduces a runtime security policy system. There is no persistent data store — all entities exist in memory (loaded at startup from configuration files) or as transient log records. + +--- + +## Entity: SecurityPolicy + +The root entity. Loaded once at JVM startup and immutable thereafter. + +| Field | Type | Description | +|-------|------|-------------| +| `permittedPaths` | `List` | File system access rules | +| `permittedEndpoints` | `List` | Outbound network access rules | +| `approvedExitCallers` | `List` | Classes allowed to call `System.exit()` / `Runtime.halt()` | +| `approvedExecCallers` | `List` | Classes allowed to spawn child processes | +| `enforcementMode` | `Enum {WARN, ENFORCE}` | Whether violations block (ENFORCE) or only log (WARN) | +| `trustedFileSystems` | `Set` | Filesystem scheme names exempt from path checks (e.g., in-memory FS used in tests) | + +**Validation rules**: +- `permittedPaths` must be non-empty (Solr home must always be in the list) +- `enforcementMode` defaults to `WARN` in the initial release +- Loaded from the active `.policy` file and Solr's runtime directory configuration +- Immutable after startup; violations of this invariant throw `SecurityException` + +**State transitions**: +- `UNINITIALIZED` → `LOADED` (at startup, before application code runs) +- No runtime modification is permitted + +--- + +## Entity: PermittedPath + +A single file system access rule. + +| Field | Type | Description | +|-------|------|-------------| +| `path` | `String` | Absolute path or path prefix (supports `${solr.home}` and similar variables) | +| `operations` | `Set` | Permitted operations on matching paths | +| `recursive` | `boolean` | Whether the rule applies to all descendants (true) or only the exact path (false) | +| `source` | `Enum {DEFAULT, OPERATOR}` | Whether the rule is from the default policy or operator extension | + +**Validation rules**: +- `path` must be an absolute path after variable substitution +- UNC paths (`\\...`) are never permitted, regardless of rules +- Symlink-resolved path must also match a `PermittedPath` rule (no escape via symlinks) +- Windows drive-relative paths (e.g., `C:relative`) are rejected + +--- + +## Entity: PermittedEndpoint + +A single outbound network access rule. + +| Field | Type | Description | +|-------|------|-------------| +| `host` | `String` | Hostname, IP address, or wildcard pattern | +| `portRange` | `String` | Port or range (e.g., `"2181"`, `"8983-8985"`, `"*"`) | +| `operations` | `Set` | Permitted socket operations | +| `source` | `Enum {DEFAULT, OPERATOR}` | Origin of the rule | + +**Validation rules**: +- Loopback addresses (`localhost`, `127.0.0.1`, `::1`) are unconditionally permitted by default +- Any host on `` is permitted by default (inter-node HTTP; covers nodes that join after startup) +- Any host on the ZooKeeper port is permitted by default: explicit config value, or ` + 1000` for embedded ZK +- External ZooKeeper ensemble hosts:ports are added at startup from the ZK connection string configuration + +--- + +## Entity: ApprovedCallSite + +A class (or class name prefix) allowed to perform a restricted operation. + +| Field | Type | Description | +|-------|------|-------------| +| `classNamePattern` | `String` | Fully-qualified class name or prefix pattern (e.g., `org.apache.solr.cli.*`) | +| `operation` | `Enum {EXIT, EXEC}` | The restricted operation this approval covers | +| `description` | `String` | Human-readable explanation of why this call site is approved | +| `source` | `Enum {DEFAULT, OPERATOR}` | Origin of the entry | + +**Default approved EXIT callers**: +- `org.apache.solr.cli.SolrCLI` (CLI shutdown commands) +- `org.apache.solr.servlet.SolrDispatchFilter` (servlet shutdown hook) + +**Default approved EXEC callers**: +- _(none by default in production policy; test policy includes `org.apache.solr.cloud.IpTables`)_ + +--- + +## Entity: SecurityViolation + +A transient record of a blocked (or warn-mode logged) operation. Not persisted; emitted as a structured log entry. + +| Field | Type | Description | +|-------|------|-------------| +| `timestamp` | `Instant` | When the violation occurred | +| `operationType` | `Enum {FILE_READ, FILE_WRITE, FILE_DELETE, NETWORK_CONNECT, SYSTEM_EXIT, PROCESS_EXEC}` | What was attempted | +| `target` | `String` | The path, host:port, or operation descriptor | +| `callSiteClass` | `String` | Top non-JDK class in the call stack at the point of violation | +| `callStack` | `List` | Abbreviated call stack for debugging | +| `mode` | `Enum {WARN, BLOCK}` | Whether this violation was logged-only or blocked | + +**Log format**: Emitted at `WARN` level (warn mode) or `ERROR` level (enforce/block mode) via SLF4J. + +--- + +## Configuration Files + +### Production Policy File + +**Location**: `${solr.home}/server/etc/agent-security.policy` +**Format**: JDK-style `.policy` syntax with Solr variable substitution +**Loaded by**: `SecurityPolicy` at agent startup + +### Operator Extension Policy + +**Location**: `${solr.home}/server/etc/agent-security-extra.policy` (optional) +**Purpose**: Operator-provided additions to the default policy +**Loaded by**: Merged with production policy at startup; operator entries tagged with `source = OPERATOR` + +### Startup Configuration + +**Location**: `${solr.home}/bin/solr.in.sh` (Linux/macOS), `bin/solr.in.cmd` (Windows) +**New variable**: `SOLR_SECURITY_AGENT_MODE` — sets enforcement mode; read by `EnvUtils`, which auto-converts env var `SOLR_SECURITY_AGENT_MODE` to system property `solr.security.agent.mode` +**Example**: +``` +SOLR_SECURITY_AGENT_MODE=warn +``` diff --git a/specs/001-jsm-replacement/plan.md b/specs/001-jsm-replacement/plan.md new file mode 100644 index 000000000000..20c8eb6f7e6a --- /dev/null +++ b/specs/001-jsm-replacement/plan.md @@ -0,0 +1,220 @@ +# Implementation Plan: Java Security Manager Replacement + +**Branch**: `15868-java-security-manager` | **Date**: 2026-04-28 | **Spec**: [spec.md](spec.md) +**JIRA**: SOLR-17767 + +--- + +## Summary + +Solr's Java Security Manager protection disappeared when JDK 24 removed the JSM API. This plan delivers a replacement using a Java agent (forked/adapted from the OpenSearch `agent-sm` module, Apache 2.0 licensed) that intercepts file access, network connections, `System.exit()`, and process spawning at the bytecode level via ByteBuddy instrumentation. The agent is activated automatically via `-javaagent:` in Solr's startup scripts and enforces a policy derived from Solr's configured directory layout. The initial release ships in **warn-only mode** to allow operator policy tuning before enabling blocking enforcement. + +--- + +## Technical Context + +**Language/Version**: Java 21 (minimum), must work on Java 24+ without deprecated APIs +**Primary Dependencies**: ByteBuddy (for bytecode instrumentation), forked from `opensearch-project/OpenSearch libs/agent-sm` (Apache 2.0) +**Storage**: No persistent storage; policy loaded from `.policy` files at JVM startup +**Testing**: JUnit 4 via Solr's randomized test framework; `SolrTestCase` / `SolrCloudTestCase` base classes +**Target Platform**: Linux, macOS, Windows (cross-platform; OS-level hardening is complementary only) +**Project Type**: Standalone Gradle subproject (`solr/agent-sm/`) + startup script integration +**Performance Goals**: ≤5% throughput degradation on standard search/indexing benchmarks +**Constraints**: Virtual-thread compatible; no `java.security.SecurityManager` API; no deprecated/removed JVM APIs +**Scale/Scope**: Applies to all code in the Solr JVM, including all loaded plugins + +--- + +## Constitution Check (AGENTS.md) + +*Re-check after Phase 1 design.* + +| Principle | Status | Notes | +|-----------|--------|-------| +| Apache License on all new source files | REQUIRED | Every new `.java` file and config file must include the ASF license header | +| All dependency versions in `gradle/libs.versions.toml` | REQUIRED | ByteBuddy version must be declared there, never in `build.gradle` directly | +| Run `gradlew updateLicenses resolveAndLockAll --write-locks` after adding ByteBuddy | REQUIRED | ByteBuddy is a new dependency; license files must be generated | +| Run `gradlew tidy` before committing Java source | REQUIRED | Code formatting enforced by Spotless | +| Run `gradlew check -x test` before declaring done | REQUIRED | Forbidden API checks, license checks, etc. | +| New test suites extend `SolrTestCase` (not `SolrTestCaseJ4`) | REQUIRED | `SolrTestCaseJ4` is deprecated for new tests | +| New classes must have javadoc | REQUIRED | Agent interceptor classes are non-obvious; javadoc is especially important | +| Wrap debug/trace log calls in `logger.isDebugEnabled()` | REQUIRED | Security interceptors are in hot paths; logging guards are essential | +| Call `coreContainer.assertPathAllowed()` for user-supplied paths | N/A | This feature replaces that pattern; new code must NOT add new call sites | +| Use `EnvUtils` to read system properties | REQUIRED | env var `SOLR_SECURITY_AGENT_MODE` → sysprop `solr.security.agent.mode` must be read via `EnvUtils` (auto-conversion is the Solr convention) | +| Use `@SuppressForbidden` for any `com.sun.*` API | LIKELY REQUIRED | ByteBuddy's bootstrap classloader injection may require `Unsafe`; needs `@SuppressForbidden` with reason | +| Changelog entry via `gradlew writeChangelog` | REQUIRED | Add after JIRA/PR is assigned | +| Reference guide update | REQUIRED | Security reference guide page needs a new section on agent-based controls | + +**No constitution violations** — all applicable principles are satisfiable within the planned approach. + +--- + +## Project Structure + +### Documentation (this feature) + +```text +specs/001-jsm-replacement/ +├── plan.md # This file +├── spec.md # Feature specification +├── research.md # Phase 0 decisions +├── data-model.md # Security policy entity model +├── quickstart.md # Operator quickstart guide +├── contracts/ +│ └── policy-file-format.md # Policy file syntax contract +└── tasks.md # Task list (52 tasks across 8 phases) +``` + +### Source Code Layout + +```text +solr/agent-sm/ +├── build.gradle # Standalone subproject build: produces agent-sm.jar with premain manifest +├── src/ +│ ├── java/org/apache/solr/security/agent/ +│ │ ├── SolrAgentEntryPoint.java # premain() + agentmain(); configures ByteBuddy +│ │ ├── SolrSecurityPolicy.java # Immutable policy singleton; loaded at startup +│ │ ├── PolicyLoader.java # Reads .policy files; variable substitution (incl. ${solr.port}, ${solr.zk.port}) +│ │ ├── FileAccessInterceptor.java # Intercepts NIO file operations; UNC + symlink handling +│ │ ├── NetworkAccessInterceptor.java # Intercepts SocketChannel.connect() / Socket.connect() +│ │ ├── ExitInterceptor.java # Intercepts System.exit() / Runtime.halt() +│ │ ├── ProcessExecInterceptor.java # Intercepts ProcessBuilder.start() / Runtime.exec() +│ │ ├── StackInspector.java # StackWalker-based call chain analysis; virtual-thread safe +│ │ ├── SecurityViolationLogger.java # Structured violation log emitter (type, target, caller, source) +│ │ └── ViolationMetricsReporter.java # LongAdder counters per type; bootstrap-safe deferred registration +│ └── test/org/apache/solr/security/agent/ +│ ├── PolicyLoaderTest.java +│ ├── FileAccessInterceptorTest.java # embedded in T026 (UNC) and T027-T028 (symlink) +│ ├── NetworkAccessInterceptorTest.java # T046 +│ ├── ExitInterceptorTest.java # T047 +│ ├── ProcessExecInterceptorTest.java # T048 +│ ├── PolicyLoaderOperatorExtensionTest.java # T025 +│ ├── UncPathRejectionTest.java # T026 +│ ├── SymlinkEscapeTest.java # T028 +│ ├── VirtualThreadCompatibilityTest.java # T030 +│ └── SolrAgentIntegrationTest.java # T024 — full-stack test with embedded Solr in enforce mode + +solr/core/src/java/org/apache/solr/core/ +└── SolrPaths.java # EXISTING: retain; add @deprecated on assertPathAllowed + +solr/server/etc/ +├── agent-security.policy # NEW: default production policy +└── agent-security-extra.policy # NEW: empty operator extension file (template) + +solr/bin/ +├── solr # MODIFY: detect agent-sm.jar, append -javaagent: +├── solr.cmd # MODIFY: same for Windows +└── solr.in.sh # MODIFY: document SOLR_SECURITY_AGENT_MODE, SOLR_SECURITY_AGENT_SKIP, SOLR_SECURITY_AGENT_EXTRA_POLICY + +solr/server/lib/ext/ +└── solr-agent-sm-*.jar # NEW: agent JAR copied here by Gradle build; detected by startup scripts + +solr/packaging/src/main/package/ +└── (MODIFY) # Add agent JAR to distribution zip/tgz + +solr/server/resources/log4j2.xml +└── (no change needed) # SecurityViolationLogger uses SLF4J → existing logging + +gradle/libs.versions.toml # ADD: byte-buddy version +``` + +--- + +## Phase 0: Research ✅ + +**Output**: [research.md](research.md) + +Key decisions made: +1. **Fork OpenSearch `agent-sm`** (Apache 2.0; same problem; same tech stack) +2. **New subproject `solr/agent-sm/`** (agent must be a standalone JAR; not a Solr module loaded inside the JVM) +3. **JDK-style `.policy` files** with Solr variable substitution +4. **Warn-only default** → enforce mode via opt-in config +5. **Auto-activation** via startup script detection of agent JAR +6. **Retain `assertPathAllowed`** as defense-in-depth; deprecate for new callers +7. **Add `ProcessExecInterceptor`** (not in OpenSearch's agent; needed for Solr) + +--- + +## Phase 1: Design & Contracts ✅ + +### Data Model + +See [data-model.md](data-model.md) for full entity definitions: +- `SecurityPolicy` — immutable root; loaded at startup +- `PermittedPath` — file system access rule (path + operations + recursive flag) +- `PermittedEndpoint` — outbound network rule (host + port range) +- `ApprovedCallSite` — allowed class for `System.exit()` or `ProcessBuilder` +- `SecurityViolation` — transient log record for blocked/warned operations + +### Contracts + +See [contracts/policy-file-format.md](contracts/policy-file-format.md): +- JDK-style `.policy` file syntax with Solr variable substitution +- Two-file approach: default policy + operator extension file +- Enforcement mode via env var `SOLR_SECURITY_AGENT_MODE` (auto-converted by `EnvUtils` to sysprop `solr.security.agent.mode`) + +### Quickstart + +See [quickstart.md](quickstart.md) — operator-facing guide covering: +- Default warn-only mode and log monitoring +- Switching to enforce mode +- Adding custom policy entries +- Diagnosing violations +- Disabling the feature (emergency only) + +### Agent Context + +See CLAUDE.md for current plan reference. + +--- + +## Implementation Phases (for /speckit-tasks) + +### Phase A: Subproject Scaffold & Policy Loading +1. Create `solr/agent-sm/build.gradle` — agent JAR with `premain`/`agentmain` manifest entries; register in `settings.gradle`; configure `copyAgentJar` task to copy output JAR to `solr/server/lib/ext/`; add agent JAR to `solr/packaging/` distribution targets +2. Add `byte-buddy` to `gradle/libs.versions.toml`; run `gradlew updateLicenses resolveAndLockAll --write-locks` +3. Implement `PolicyLoader` — reads and parses `.policy` files; Solr variable substitution +4. Implement `SolrSecurityPolicy` — immutable singleton; merges default + extra policy +5. Write unit tests for `PolicyLoader` (valid policy, malformed policy, missing file → startup failure) + +### Phase B: File Access Enforcement +1. Adapt OpenSearch `FileInterceptor` → `FileAccessInterceptor` for Solr package structure +2. Add ASF license headers; write javadoc on all public classes +3. Handle UNC path rejection and symlink resolution +4. Write unit tests covering: permitted read, denied read, UNC path, symlink escape, warn vs. enforce mode + +### Phase C: Network Access Enforcement +1. Adapt OpenSearch `SocketChannelInterceptor` → `NetworkAccessInterceptor` +2. Add `${solr.port}` and `${solr.zk.port}` variable support to `PolicyLoader`; include `*:${solr.port}` and `*:${solr.zk.port}` wildcard entries in the default policy (Decision 9 — covers all cluster nodes regardless of join order; ZK port = explicit config or `solr.port + 1000`) +3. Write unit tests covering: loopback permitted, port-wildcard entry permits any host on that port, unlisted host:port blocked + +### Phase D: Exit & Process Enforcement +1. Adapt OpenSearch `SystemExitInterceptor` + `RuntimeHaltInterceptor` → `ExitInterceptor` +2. Implement `ProcessExecInterceptor` (new — not in OpenSearch agent) for `ProcessBuilder` +3. Define default approved exit callers; define empty default exec allow-list +4. Write unit tests for each interceptor + +### Phase E: Agent Entry Point & Startup Integration +1. Implement `SolrAgentEntryPoint.premain()` — registers all ByteBuddy interceptors; loads policy +2. Modify `solr/bin/solr` and `solr/bin/solr.cmd` to detect agent JAR and add `-javaagent:` +3. Create `solr/server/etc/agent-security.policy` (default) and `agent-security-extra.policy` (empty template) +4. Write integration test: start embedded Solr with agent active; verify normal ops succeed; verify test-plugin violations are caught + +### Phase F: Documentation & Cleanup +1. Add changelog entry via `gradlew writeChangelog` +2. Update reference guide: security section with new agent-based controls +3. `@Deprecated` on `SolrPaths.assertPathAllowed` with migration note +4. Run `gradlew tidy check -x test` and fix any issues +5. Update `dev-docs/` with developer notes on adding new approved call sites + +--- + +## Risks & Mitigations + +| Risk | Likelihood | Mitigation | +|------|-----------|------------| +| ByteBuddy instrumentation breaks virtual threads | Low | OpenSearch already validated this; Solr tests will run with virtual threads enabled | +| Default policy too restrictive → test failures | Medium | All existing tests run with agent in warn mode first; violations triaged before switching to enforce | +| ByteBuddy bootstrap classloader injection rejected by JVM security flags | Low | Same technique used by OpenSearch 3.0 on Java 21/24; tested against Solr's startup flags | +| Performance regression >5% | Low | File access on hot search paths is read-only; agent interceptor is a lightweight stack walk | +| Plugin ecosystem breakage | Medium | Warn-only default gives plugin authors time to add policy extension documentation | diff --git a/specs/001-jsm-replacement/quickstart.md b/specs/001-jsm-replacement/quickstart.md new file mode 100644 index 000000000000..4e9b786a9fd1 --- /dev/null +++ b/specs/001-jsm-replacement/quickstart.md @@ -0,0 +1,106 @@ +# Quickstart: Java Security Manager Replacement + +**Audience**: Solr operators and plugin developers +**Feature**: Agent-based runtime security controls replacing the removed Java Security Manager + +--- + +## What This Feature Does + +Starting with this release, Solr automatically enforces boundaries around: +- **File system access** — code can only read/write directories Solr is configured to use +- **Network connections** — outbound connections are restricted to known cluster endpoints +- **JVM shutdown** — `System.exit()` can only be called from Solr's own shutdown paths +- **Process spawning** — child process creation is restricted to approved Solr components + +These protections apply to all code in the JVM, including third-party plugins, **without any code changes required**. + +--- + +## Default Behaviour + +### Enforcement mode at startup + +In this release, the default mode is **warn-only**: violations are logged but operations are not blocked. This allows operators to identify any gaps in the default policy before switching to enforce mode. + +Check for violations in `solr.log`: +``` +grep "SECURITY VIOLATION" logs/solr.log +``` + +### Switching to enforce mode + +Once satisfied that no legitimate operations are being flagged, enable enforcement: + +In `bin/solr.in.sh` (Linux/macOS): +```bash +SOLR_SECURITY_AGENT_MODE=enforce +``` + +In `bin/solr.in.cmd` (Windows): +```bat +set SOLR_SECURITY_AGENT_MODE=enforce +``` + +Then restart Solr. + +--- + +## Extending the Policy for Custom Plugins + +If your plugin reads from a non-standard directory or connects to an external service, add a policy extension file: + +**Default file**: `server/etc/agent-security-extra.policy` + +**Custom location** (read-only installs, containers, config-management): +```bash +# In bin/solr.in.sh +SOLR_SECURITY_AGENT_EXTRA_POLICY=/etc/solr/my-security-extra.policy +``` +An absent file is silently skipped — no need to create it if no extensions are required. + +**Example** — plugin reads from `/data/external` and connects to `reporting.myco.com:8080`: +``` +grant { + permission java.io.FilePermission "/data/external/-", "read"; + permission java.net.SocketPermission "reporting.myco.com:8080", "connect,resolve"; +}; +``` + +Restart Solr after editing. The extension file is merged with the default policy at startup. + +--- + +## Diagnosing Violations + +A violation log entry looks like: +``` +[WARN ] SecurityAgent - SECURITY VIOLATION [FILE_READ] target=/tmp/evil.txt caller=com.example.MyPlugin mode=WARN +``` + +**To identify the source**: The `caller` field shows the top non-JDK class in the stack. If it belongs to a known plugin, add a policy entry for that plugin's required access. + +**To see the full call stack**: Enable DEBUG logging for `org.apache.solr.security.agent`. + +--- + +## Disabling the Feature (Not Recommended) + +If you need to temporarily disable security controls during troubleshooting: + +```bash +# In bin/solr.in.sh — removes the -javaagent flag +SOLR_SECURITY_AGENT_SKIP=true +``` + +This is a temporary measure only. Running without security controls on Java 24+ provides no runtime enforcement of any kind. + +--- + +## Plugin Author Guide + +**No code changes required** for plugins that access standard Solr-managed paths and cluster endpoints. + +**If your plugin accesses external resources**, document the required policy entries in your plugin's README so operators can add them to `agent-security-extra.policy`. + +**Do not** call `SolrPaths.assertPathAllowed()` in new plugin code — the agent enforces this automatically. The method is retained for internal Solr use only. diff --git a/specs/001-jsm-replacement/research.md b/specs/001-jsm-replacement/research.md new file mode 100644 index 000000000000..99b91521dc04 --- /dev/null +++ b/specs/001-jsm-replacement/research.md @@ -0,0 +1,241 @@ +# Phase 0 Research: Java Security Manager Replacement + +**Date**: 2026-04-28 +**Feature**: specs/001-jsm-replacement/spec.md + +--- + +## Decision 1: Adopt, Fork, or Build the Java Agent + +**Decision**: Fork/adapt the OpenSearch `agent-sm` module rather than building from scratch. + +**Rationale**: +- The OpenSearch agent is Apache 2.0 licensed — fully compatible with Apache Solr's licensing. +- It directly solves the same problem (JSM removal in JDK 24), with the same four protection categories required by this spec: file access, network access, System.exit(), process execution. +- It uses ByteBuddy (already widely used in the Java ecosystem) and StackWalker (standard since Java 9), both compatible with Java 21+. +- It is virtual-thread compatible (no thread-identity assumptions). +- Building equivalent infrastructure from scratch would take significantly longer with no functional advantage. +- Solr can strip out OpenSearch-specific policy entries (e.g., BouncyCastle, script permissions) and adapt the policy defaults to Solr's directory layout. + +**Alternatives considered**: +- **Build from scratch**: Higher effort, same outcome, no benefit. +- **Use a third-party security library (e.g., Byte Buddy agent SPI)**: No existing library covers the full set of interceptions needed (file + network + exit + exec). Would still require significant custom work. +- **Rely solely on OS-level hardening (systemd)**: Fails the cross-platform constraint (not available on Windows/macOS). + +--- + +## Decision 2: Subproject Location + +**Decision**: Create a new Gradle subproject `solr/agent-sm/` (directly under `solr/`, not under `solr/modules/`). + +**Rationale**: +- The agent JAR must be on the JVM command line as `-javaagent:`, loaded by the JVM *before* any application classloader. It is fundamentally different from a Solr module (which is loaded inside the running Solr JVM by Solr's own classloading machinery). +- Placing it under `solr/modules/` would be misleading — Solr modules are components that Solr itself loads and manages at runtime. The agent is an external JVM concern that Solr does not load; the JVM does. +- `solr/agent-sm/` is structurally parallel to other non-module subprojects like `solr/core/`, `solr/solrj/`, and `solr/test-framework/`, which is the correct category for a build artifact that is JVM-level infrastructure. +- Mirroring the OpenSearch layout (`libs/agent-sm/`) is closest in spirit, but Solr does not use a top-level `libs/` directory; `solr/` is the equivalent container. + +**Alternatives considered**: +- **`solr/modules/agent-sm/`**: Misleading — modules are Solr-level, not JVM-level. Would also imply the agent is optional/pluggable in the same sense as, e.g., `jwt-auth`, which it is not. +- **`solr/core/` integration**: Not possible — agent must be a standalone fat JAR with `Premain-Class` manifest entry, produced independently of `solr-core.jar`. +- **Top-level `agent-sm/`**: Would sit outside the `solr/` tree, inconsistent with where all Solr Java source lives. + +--- + +## Decision 3: Policy Configuration Format + +**Decision**: Use JDK-style `.policy` file syntax (same format as the existing `solr/server/etc/security.policy`), extended with Solr-specific variable substitution (e.g., `${solr.home}`, `${solr.data.dir}`). + +**Rationale**: +- Solr already ships a `security.policy` file; operators are familiar with the syntax. +- OpenSearch's `agent-sm` already implements a `PolicyFile` parser that reads JDK-style `.policy` files — this can be adapted directly. +- Solr-specific variable substitution (`${solr.home}` etc.) avoids hardcoding paths and makes the default policy portable across installations. + +**Alternatives considered**: +- **Custom YAML/JSON config**: More readable but requires a new parser, new documentation, and breaks familiarity with JSM policy syntax. +- **Inline configuration in `solr.xml`**: Too verbose; policy files can be large; separating concerns is cleaner. + +--- + +## Decision 4: Enforcement Mode — Enforce vs. Warn-Only + +**Decision**: Ship with a **warn-only mode** as the initial default, with an **enforce mode** opt-in via configuration. A future release will flip the default to enforce. + +**Rationale**: +- Solr has a large plugin ecosystem; some plugins may legitimately access paths or hosts not covered by the default policy. +- A hard-blocking default would cause regressions for valid third-party plugins, violating FR-013. +- Warn-only allows operators to identify gaps in their custom policy before flipping to enforce. +- This matches how the old JSM was typically introduced: test first, enforce later. + +**Alternatives considered**: +- **Enforce by default immediately**: Higher risk of regressions for plugin authors; may delay adoption. +- **Warn-only permanently**: Defeats the security purpose; not acceptable long term. + +--- + +## Decision 9: Default Network Policy for Intra-Cluster Connectivity + +**Decision**: The default permitted network endpoint list uses **port-based wildcards** rather than a host-derived list: permit any host on `` (inter-node HTTP) and any host on the ZooKeeper port (explicit config, or ` + 1000` for embedded ZK). + +**Rationale**: +- SolrCloud nodes join the cluster incrementally after ZooKeeper registers them. A node list derived at agent startup will always be incomplete, causing legitimate inter-node connections to be blocked in enforce mode for nodes that join later. +- Port-based wildcards allow all current and future cluster nodes to communicate without any operator action or restart. +- The port restriction still prevents connections to arbitrary external services — an attacker or rogue plugin cannot use the intra-cluster policy to reach, e.g., a database on an unexpected port. +- Inter-node Solr HTTP is already authenticated via PKI, so the broadened host permission does not weaken authentication. +- This removes the previously documented "known limitation" that topology changes require a restart. + +**Alternatives considered**: +- **Dynamic ZK watcher**: Subscribe to ZK node join/leave events and update permitted endpoints at runtime. Tightest security, but adds significant complexity to the agent and creates a dependency on ZK client libraries in the agent JAR. +- **Auto-detect local subnet**: Derive `/24` from the primary network interface. Avoids needing to know the port but may be ambiguous on multi-homed hosts or containers. +- **Operator-configured subnet** (`SOLR_SECURITY_AGENT_CLUSTER_SUBNET`): Explicit but adds operator friction; incorrect configuration silently breaks the cluster. +- **Startup-only host derivation**: Rejected because in practice the ZK-known node list is always incomplete at startup. + +**Default permitted entries added by this decision**: +- `*:` — connect,resolve (inter-node Solr HTTP; default port 8983) +- `*:` — connect,resolve (ZK port from config, or `solr.port + 1000` for embedded ZK) +- External ZK ensemble hosts:ports from the ZK connection string (specific hosts, added at startup) + +--- + +## Decision 5: Activation Mechanism + +**Decision**: The agent JAR is added to Solr's startup scripts (`solr.in.sh` / `solr.in.cmd`) as a `-javaagent:` JVM argument, automatically, when the module is present. + +**Rationale**: +- Java agents must be loaded via `-javaagent:` before the application starts — they cannot be loaded on demand at runtime. +- Solr's startup scripts already have a pattern for conditionally appending JVM args (e.g., for GC logging, heap dumps). +- The module JAR being present in `server/modules/agent-sm/` is sufficient signal; the startup script detects it and adds the flag. + +**Alternatives considered**: +- **Always-on (bundled in core)**: Not technically feasible for a Java agent. +- **Manual operator configuration of JVM args**: Too error-prone; operators would forget; protections would silently not apply. + +--- + +## Decision 6: Disposition of SolrPaths.assertPathAllowed + +**Decision**: Retain all existing `SolrPaths.assertPathAllowed` call sites as defense-in-depth, but document them as no longer the primary enforcement mechanism. + +**Rationale**: +- Removing 35 call sites is unnecessary churn and removes defense-in-depth for code paths that may not be instrumented. +- The agent-based enforcement is the primary layer; `assertPathAllowed` becomes a secondary redundant check. +- New code MUST NOT add new `assertPathAllowed` call sites — the agent handles it automatically. + +--- + +## Decision 7: Process Execution Control Implementation + +**Decision**: Instrument `ProcessBuilder` and `Runtime.exec()` at the bytecode level (via agent) to restrict spawning to an allow-listed set of call-site class name prefixes. The default allow-list covers Solr's known legitimate process-spawning call sites. + +**Rationale**: +- Existing `ProcessBuilder` usage in Solr is minimal (3 sites: SolrProcessManager, IpTables test helper). +- OpenSearch's `agent-sm` does not include a ProcessBuilder interceptor — Solr will need to add one. +- Class-prefix allow-listing is simple, auditable, and requires no runtime performance cost for non-spawning code paths. + +**Known approved call sites**: +- `org.apache.solr.cli.SolrProcessManager` (JVM discovery via ProcessHandle — does not use ProcessBuilder directly; kept as approved for future compatibility) +- `org.apache.solr.cloud.IpTables` (test-only; listed in test policy, not production policy) + +--- + +## Decision 8: Metrics Registration Bridge (Classloader Boundary) + +**Decision**: Use reflection-based late binding from `CoreContainer` to `ViolationMetricsReporter`. No compile-time dependency between `solr:core` and `solr:agent-sm` is introduced. + +**Rationale**: +- The Java agent runs in the bootstrap classloader (injected via `ClassInjector.UsingUnsafe.ofBootLoader()`). Classes loaded there are visible to all classloaders, including the application classloader used by `solr:core`. +- A compile-time dependency `solr:core → solr:agent-sm` would be circular if agent-sm ever needs core types, and would also put agent classes on the application classpath rather than the bootstrap classpath — defeating the instrumentation model. +- Reflection avoids the compile-time coupling while still allowing CoreContainer to call `ViolationMetricsReporter.registerWithSolrMetrics()` at runtime if and only if the agent is loaded. +- `ClassNotFoundException` is caught silently: if the agent JAR is not present (e.g., `SOLR_SECURITY_AGENT_SKIP=true`), metrics registration is simply skipped. No NPE, no startup failure. + +**Implementation pattern** (in `CoreContainer`): +```java +try { + Class reporter = Class.forName( + "org.apache.solr.security.agent.ViolationMetricsReporter", + false, + null // null = bootstrap classloader + ); + reporter.getMethod("registerWithSolrMetrics", SolrMetricManager.class, String.class) + .invoke(null, metricManager, "solr.jvm"); +} catch (ClassNotFoundException ignored) { + // Agent not loaded; metrics registration skipped +} catch (ReflectiveOperationException e) { + log.warn("Failed to register security agent metrics", e); +} +``` + +**Alternatives considered**: +- **Shared interface module `solr:agent-sm-api`**: Both `solr:core` and `solr:agent-sm` would depend on it. Cleaner but adds a new module with a single interface. Overkill for one method; deferred to future if the API grows. +- **ServiceLoader SPI**: Requires the agent JAR on the application classpath, not bootstrap. Architecturally incorrect for a Java agent. +- **Direct compile-time dependency**: Circular; breaks bootstrap classloader model. Rejected. + +--- + +## Codebase Findings Summary + +| Area | Finding | +|------|---------| +| Java minimum | 21 (`gradle/libs.versions.toml`: `java-min = "21"`) | +| Existing policy files | `solr/server/etc/security.policy`, `gradle/testing/randomization/policies/solr-tests.policy` | +| SolrPaths.assertPathAllowed | `solr/core/src/java/org/apache/solr/core/SolrPaths.java`; 35 call sites | +| ProcessBuilder usage | 3 sites (SolrProcessManager, IpTables test helper, ProcessManager test) | +| Subproject pattern reference | `solr/core/`, `solr/solrj/` (agent is JVM-level, not a Solr module) | +| Test base classes | `SolrTestCase`, `SolrCloudTestCase` | +| No SecurityManager active | Policy files exist but no `System.setSecurityManager()` calls | +| No existing security module | Closest is `solr/modules/jwt-auth/` | + +--- + +## Codebase Finding: Module Outbound Network Connections + +Analysis of server-side outbound network calls (excluding ZooKeeper and inter-Solr-node HTTP, which are covered by the default policy). This informs T044 (module ref guide NOTICE boxes) and T045 (commented examples in `agent-security-extra.policy`). + +| Module | External Service | Protocol | Component | Notes | +|--------|----------------|----------|-----------|-------| +| `jwt-auth` | OIDC Identity Provider (well-known + JWKS) | HTTPS | `JWTIssuerConfig`, `HttpsJwksFactory` | Uses `jose4j`; host is operator-configured; may refresh JWKS at runtime | +| `extraction` | Apache Tika Server | HTTP/HTTPS | `TikaServerExtractionBackend` | Uses Jetty `HttpClient`; only in remote-Tika mode — embedded Tika makes no outbound calls | +| `opentelemetry` | OTLP collector (Jaeger, Grafana Tempo, etc.) | gRPC or HTTP | `OtlpExporterFactory` | Host from `OTEL_EXPORTER_OTLP_ENDPOINT`; continuous background export | +| `s3-repository` | Amazon S3 API (or S3-compatible) | HTTPS | `S3StorageClient` | AWS SDK v2; uses `*.amazonaws.com` or operator-configured custom endpoint | +| `gcs-repository` | Google Cloud Storage API | HTTPS | `GCSBackupRepository` | GCS SDK; uses `storage.googleapis.com` | +| `cross-dc-manager` | Apache Kafka brokers | TCP (Kafka protocol) | `KafkaCrossDcConsumer`, `KafkaRequestMirroringHandler` | Broker addresses fully operator-configured | +| `core` (ZK status admin) | ZooKeeper four-letter-word socket | TCP (raw socket, port 2181) | `ZookeeperStatusHandler` | Same ZK hosts as cluster; already covered by the default ZK policy entry | + +**No outbound network calls found in**: `langid` (local NLP only), `ltr` (local feature processing), `BasicAuthPlugin` (in-memory), `analytics`. + +### Handling approach — superseded by Decision 10 + +See Decision 10 below. The original plan (NOTICE boxes for all 6 modules) was refined after per-module trust assessment to pre-permit 5 of the 6 modules directly in the bundled agent policy. + +--- + +## Decision 10: Per-Module Wildcard Network Pre-Permitting + +**Decision**: For five of the six outbound-connecting bundled modules, add a `SocketPermission "*", "connect,resolve"` grant scoped to that module's codebase in the bundled agent policy (`agent-security.policy`). The `extraction` module is excluded. + +**Pre-permitted modules and rationale**: + +| Module | External Service | Who Configures Endpoint | Trust Assessment | +|--------|----------------|------------------------|-----------------| +| `jwt-auth` | OIDC IdP + JWKS | `security.json`, requires `SECURITY_EDIT` (Solr admin) | **Safe** — admin-only | +| `opentelemetry` | OTLP collector | Env var / system property / solr.xml — node admin only | **Safe** — node admin only | +| `s3-repository` | Amazon S3 / S3-compat | solr.xml backup handler config — node admin | **Safe** — node admin only | +| `gcs-repository` | Google Cloud Storage | solr.xml backup handler config — node admin | **Safe** — node admin only | +| `cross-dc-manager` | Apache Kafka | Env var / system property / ZooKeeper — node + cluster admin | **Safe** — admin-only | +| `extraction` | Remote Tika Server | `solrconfig.xml` `requestHandler` — `CONFIG_EDIT` privilege (collection admin) | **SSRF risk** — excluded | + +**Why `extraction` is excluded**: The Tika Server URL is set in `solrconfig.xml` via `TikaConfig`, which is modifiable by collection administrators with `CONFIG_EDIT` privilege — a broader group than node administrators. A wildcard `SocketPermission` would allow a collection admin to point the Tika client at any internal host, enabling SSRF attacks against the cluster's internal network from within the Solr JVM. Operators who use remote Tika must add an explicit host-locked policy entry to `agent-security-extra.policy`. + +**Implementation**: Each pre-permitted module's bundled policy grant uses a `codeBase` restriction to limit the wildcard to that module's JAR: +``` +grant codeBase "file:${solr.install.dir}/modules/jwt-auth/-" { + permission java.net.SocketPermission "*", "connect,resolve"; +}; +``` +This prevents other code from benefiting from the module's wildcard grant. + +**Effect on T044/T045**: Scope is reduced to the `extraction` module only. No NOTICE boxes or `agent-security-extra.policy` examples are needed for the five pre-permitted modules. + +**Alternatives considered**: +- **NOTICE boxes for all 6 modules**: Higher operator friction; legitimate use cases (e.g., OTel metrics export) require manual action on every new cluster. Rejected. +- **Wildcard grant for all 6 including extraction**: Creates SSRF risk for the extraction module. Rejected. +- **Per-hostname grants auto-derived from config at startup**: Config is not available to the agent at JVM launch time; would require complex late-binding. Rejected. diff --git a/specs/001-jsm-replacement/spec.md b/specs/001-jsm-replacement/spec.md new file mode 100644 index 000000000000..00e90023378f --- /dev/null +++ b/specs/001-jsm-replacement/spec.md @@ -0,0 +1,190 @@ +# Feature Specification: Java Security Manager Replacement + +**Feature Branch**: `15868-java-security-manager` +**Created**: 2026-04-28 +**Status**: Draft +**JIRA**: SOLR-17767 + +## Background + +Java Security Manager (JSM) was removed in JDK 24. Solr historically relied on it to enforce boundaries around file access, network traffic, process execution, and JVM lifecycle operations. Solr currently has a partial mitigation (`SolrPaths.assertPathAllowed`) for file paths, but it requires developers to call it explicitly — nothing enforces that they do. There is no equivalent mitigation for network access, Windows UNC path attacks, `System.exit()` abuse, or arbitrary process spawning. + +This feature introduces automatic, cross-platform runtime security controls covering the highest-risk categories: file system access, outbound network connections, JVM shutdown prevention, and process execution restriction. + +--- + +## User Scenarios & Testing + +### User Story 1 — Standard Solr Deployment Is Protected by Default (Priority: P1) + +A Solr operator upgrades to a Java 24+ JVM (or any supported JVM). Without any configuration change, the new security controls automatically enforce boundaries around file access, network calls, `System.exit()`, and process spawning. Plugins loaded by Solr are also subject to these controls. + +**Why this priority**: This is the core goal of the feature. All existing Solr deployments should gain protection automatically — no manual action required. + +**Independent Test**: Deploy a standard Solr instance. Load a test plugin that attempts to read a file outside Solr's permitted directories. Verify the read is blocked and a warning is logged. Verify all standard Solr operations (search, indexing, admin) continue to function normally. + +**Acceptance Scenarios**: + +1. **Given** a Solr server running with default configuration, **When** any code (including a plugin) attempts to read a file outside the permitted directory tree, **Then** the operation is denied and a log entry identifies the blocked path and the call origin. +2. **Given** a Solr server running with default configuration, **When** any code attempts to open an outbound network connection to a host not in the permitted list, **Then** the connection is refused and logged. +3. **Given** a Solr server running with default configuration, **When** plugin code calls `System.exit()`, **Then** the JVM does not terminate and the violation is logged. +4. **Given** a Solr server running with default configuration, **When** code attempts to spawn a child process from an unauthorized call site, **Then** the process is not created and the violation is logged. +5. **Given** a Solr server running with default configuration, **When** a standard search or indexing request is processed, **Then** all operations complete successfully with no security violations triggered. + +--- + +### User Story 2 — Operator Extends Policy for Custom Deployment (Priority: P2) + +A Solr operator runs a non-standard deployment: for example, a plugin that legitimately reads from an external NFS mount, or a connector that makes outbound HTTP calls to a specific SaaS endpoint. The operator needs to authorize these operations without disabling security controls globally. + +**Why this priority**: Real-world deployments have legitimate needs beyond the default policy. Without this, operators would be forced to disable the feature entirely. + +**Independent Test**: Configure a custom policy entry that permits a specific additional file path. Verify that access to that path succeeds. Verify that access to a different unauthorized path is still blocked. + +**Acceptance Scenarios**: + +1. **Given** an operator has added a permitted file path to the policy configuration, **When** Solr code reads a file at that path, **Then** the read succeeds. +2. **Given** an operator has permitted a specific outbound host, **When** a plugin connects to that host, **Then** the connection succeeds. +3. **Given** an operator has not permitted a path or host, **When** code attempts to access it, **Then** the access is denied regardless of any custom entries. + +--- + +### User Story 3 — Windows UNC Paths Are Blocked (Priority: P2) + +An attacker or misconfigured plugin constructs a Windows UNC path (`\\attacker-host\share\...`) to exfiltrate data or probe internal networks. On a Windows host, the OS would initiate an outbound SMB connection; on any host, UNC paths bypass standard path validation. + +**Why this priority**: UNC paths are a well-known bypass vector for path-based access controls. The current `SolrPaths.assertPathAllowed` does not protect against them. + +**Independent Test**: Attempt a file read using a UNC-style path. Verify the operation is denied and logged on all platforms. + +**Acceptance Scenarios**: + +1. **Given** any Solr deployment on any supported platform, **When** code attempts to access a path beginning with `\\`, **Then** the access is denied and logged. +2. **Given** a legitimate file read using a standard absolute path, **When** the path resolves within permitted directories, **Then** the read succeeds normally. + +--- + +### User Story 4 — Symlink Escape Is Prevented (Priority: P2) + +An attacker or plugin creates a symlink inside a permitted directory that points to a sensitive path outside it (e.g., `/etc/passwd`). Under the old JSM, following symlinks into unauthorized directories was blocked. This protection must continue. + +**Why this priority**: Symlink attacks are a classic privilege escalation path that specifically targets path-based access controls. + +**Independent Test**: Create a symlink inside the Solr data directory pointing to `/etc/passwd`. Attempt to read via the symlink. Verify the read is denied. + +**Acceptance Scenarios**: + +1. **Given** a symlink inside a permitted directory pointing to a path outside permitted directories, **When** code follows the symlink and reads the target, **Then** the read is denied and logged. + +--- + +### User Story 5 — Security Controls Are Compatible with Virtual Threads (Priority: P3) + +A Solr deployment uses Project Loom virtual threads. The security controls must not interfere with virtual thread scheduling or produce incorrect enforcement decisions based on thread identity. + +**Why this priority**: Virtual thread compatibility is a hard constraint — Solr is moving toward Loom and JSM itself was incompatible with it. + +**Independent Test**: Enable virtual threads; run a standard workload. Verify no false positives (legitimate operations blocked) or false negatives (violations not caught). + +**Acceptance Scenarios**: + +1. **Given** Solr is running with virtual threads enabled, **When** standard operations execute, **Then** no security violations are incorrectly triggered. +2. **Given** a virtual thread executes unauthorized file access, **When** the access is attempted, **Then** it is blocked just as it would be on a platform thread. + +--- + +### Edge Cases + +- What happens when a security violation occurs in a critical path during startup? The violation must be logged and startup must fail fast with a clear error rather than silently continuing. +- How does the system behave when the policy configuration file is absent or malformed? Solr must refuse to start and log a clear error rather than running without any security controls. +- What happens when a permitted path is deleted at runtime? Access to paths that were permitted but no longer exist must still be governed by the policy (i.e., a missing permitted path does not grant access to its parent). +- What happens on platforms where process spawning is used internally by Solr (e.g., the `bin/solr` script launcher)? Approved internal call sites must be explicitly listed in the default policy. + +--- + +## Requirements + +### Functional Requirements + +- **FR-001**: The system MUST automatically apply security controls to all code running in the Solr JVM, including third-party plugins, without requiring developers to add per-call-site checks. +- **FR-002**: The system MUST deny file reads and writes to paths outside the set of permitted directories derived from Solr's configured home, data, log, and temporary directories. +- **FR-003**: The system MUST deny file access to Windows UNC paths (`\\host\share\...`) on all platforms by default. +- **FR-004**: The system MUST deny file access when a symlink resolves to a target outside the permitted directories. +- **FR-005**: The system MUST deny outbound network connections to hosts and ports not declared in the permitted endpoint list. The default list MUST include: loopback addresses, any host on the configured Solr HTTP port (`*:`), any host on the ZooKeeper port (explicit config or ` + 1000` for embedded ZK), and a wildcard `SocketPermission "*", "connect,resolve"` scoped to the codebases of the five pre-permitted bundled modules (`jwt-auth`, `opentelemetry`, `s3-repository`, `gcs-repository`, `cross-dc-manager`), so that all current and future cluster nodes and these trusted modules are reachable without operator intervention. +- **FR-006**: The system MUST prevent `System.exit()` from terminating the JVM except when called from a small, explicitly approved set of Solr shutdown call sites. +- **FR-007**: The system MUST prevent spawning of child processes except from an explicitly approved, auditable set of call sites in Solr core. +- **FR-008**: Every security violation MUST produce a log entry at WARN level or higher that identifies: the blocked operation type, the target (path, host, or operation), and sufficient call-site context to locate the offending code. +- **FR-009**: Operators MUST be able to extend the default security policy by adding permitted file paths, network endpoints, or approved call sites via the `agent-security-extra.policy` file, without modifying Solr source code. The path to this file MUST default to `${server.dir}/etc/agent-security-extra.policy` and MUST be overridable via the `SOLR_SECURITY_AGENT_EXTRA_POLICY` environment variable (auto-converted to system property `solr.security.agent.extra.policy`). An absent extra policy file MUST be silently skipped; it is not a startup error. +- **FR-010**: The security controls MUST function correctly on Java 21 and Java 24+, without relying on any deprecated or removed JVM API. +- **FR-011**: The security controls MUST be compatible with Project Loom virtual threads; enforcement decisions MUST NOT depend on thread identity. +- **FR-012**: The security controls MUST NOT cause a measurable throughput degradation of more than 5% on standard Solr search and indexing workloads compared to an equivalent deployment without controls. +- **FR-013**: All Solr functionality that works in a standard deployment today MUST continue to work without modification when security controls are enabled. +- **FR-014**: The security controls MUST be cross-platform; they MUST apply on Linux, macOS, and Windows without relying on OS-specific mechanisms as the sole enforcement layer. +- **FR-015**: The system MUST refuse to start if the security policy configuration is absent or invalid, and MUST log a clear diagnostic message. +- **FR-016**: The agent security controls and their configuration (policy file format, enforcement mode, operator extension mechanism, and startup options) MUST be documented in the Solr reference guide as a dedicated section visible to end users and operators. Additionally, all new environment variables and system properties introduced by this feature (`SOLR_SECURITY_AGENT_MODE` / `solr.security.agent.mode`, `SOLR_SECURITY_AGENT_SKIP`, `SOLR_SECURITY_AGENT_EXTRA_POLICY` / `solr.security.agent.extra.policy`) MUST be added to `solr-properties.adoc` with their default values and descriptions, consistent with the format of existing entries in that file. +- **FR-017**: The system MUST expose security violation counts, broken down by type (file access, network, exit, process exec), in Solr's metrics registry so operators can monitor and alert on violations without log parsing. + +### Key Entities + +- **Security Policy**: The complete set of rules defining what file paths, network endpoints, and process operations are permitted. Has a default configuration and supports operator-defined extensions. +- **Permitted Path**: A directory (or file pattern) that code running in the Solr JVM is allowed to read from or write to. Derived from Solr's configured layout by default. +- **Permitted Endpoint**: A host-and-port pair (or pattern) that code is allowed to connect to outbound. The default list covers intra-cluster traffic via port-based wildcards (`*:`, `*:`); all other external traffic requires explicit operator policy entries. +- **Approved Call Site**: A specific class (or class pattern) in Solr core that is permitted to call `System.exit()` or spawn a child process. +- **Security Violation**: A blocked operation. Carries operation type, target, timestamp, and call-site context. Emitted as a log entry. + +--- + +## Success Criteria + +### Measurable Outcomes + +- **SC-001**: 100% of standard Solr integration tests pass without modification when security controls are enabled, on both Java 21 and Java 24+. +- **SC-002**: Throughput on standard search and indexing benchmarks degrades by no more than 5% with controls enabled versus disabled. +- **SC-003**: Every attempted unauthorized file access, outbound connection, `System.exit()` call, and unauthorized process spawn produces a log entry within the same operation, on 100% of attempts in automated tests. +- **SC-004**: A plugin attempting to read a file outside the permitted directory tree is blocked on 100% of attempts, including when the path uses symlinks or Windows UNC notation. +- **SC-005**: An operator can configure a custom permitted file path or network endpoint and have it take effect after a Solr restart, following only the published documentation. +- **SC-006**: Solr starts and operates normally under virtual threads with security controls enabled, with no false-positive security violations in the standard test suite. +- **SC-007**: After at least one security violation occurs, the corresponding per-type counter in the metrics registry is non-zero and readable via the standard metrics API, confirming metric registration succeeded. +- **SC-008**: The `solr/agent-sm/` module test suite runs with the agent in enforce mode and passes with zero unexpected violations; the broader Solr test suite runs in warn mode with zero unexpected violations logged. + +--- + +## Clarifications + +### Session 2026-04-28 + +- Q: Must the agent and its configuration be documented in the Solr reference guide? → A: Yes — FR-016 added requiring a dedicated reference guide section covering policy file format, enforcement mode, operator extension mechanism, and startup options. +- Q: Should security violations be exposed as Solr metrics in addition to log entries? → A: Yes, broken down by type (FILE, NETWORK, EXIT, EXEC) in the metrics registry — FR-017 added. +- Q: Should the agent run in enforce mode during the Solr test suite? → A: Enforce mode within the `solr/agent-sm/` module tests only; warn mode for the broader Solr test suite initially, with a planned follow-up to flip to enforce once the violation log is clean. +- Q: Should the network policy refresh when SolrCloud cluster topology changes at runtime? → A: No — startup-only; topology changes require a restart. Documented as a known limitation; workaround is permitting cluster subnet via the extension policy file. *(Superseded by Q7 below.)* +- Q: How should the default network policy handle SolrCloud inter-node connectivity, given that the full node set is not known at agent startup? → A: Permit any host on the configured Solr HTTP port (`*:`) and on the ZooKeeper port (explicit config or `solr.port + 1000` for embedded ZK). No host restriction for intra-cluster traffic; port restriction prevents connections to arbitrary external services on other ports. The startup-only topology derivation assumption is removed; no restart is required when new nodes join the cluster. +- Q: What happens to the existing JSM-era policy files (`security.policy`, `solr-tests.policy`)? → A: Retain both with deprecation notices in this release; schedule removal in a future release. +- Q: When does warn mode become enforce-by-default, and should warn mode log a startup advisory? → A: No committed timeline; the flip is a future decision. Warn mode does not log an advisory message. + +### Session 2026-04-29 + +- Q: Should bundled modules that make outbound network connections have their endpoint host pre-permitted in the default agent policy, or should each require an explicit operator `agent-security-extra.policy` entry? → A: Pre-permit with a `SocketPermission "*", "connect,resolve"` wildcard in the **bundled agent policy** for the five modules whose external endpoint is exclusively controlled by a node/cluster admin (`jwt-auth`, `opentelemetry`, `s3-repository`, `gcs-repository`, `cross-dc-manager`). The `extraction` module is excluded from this pre-permitting because its Tika Server URL is configurable via `solrconfig.xml` at `CONFIG_EDIT` privilege level, creating an SSRF risk with a wildcard grant; operators must add an explicit policy entry for it. T044 and T045 scope is reduced to the `extraction` module only. + +### Session 2026-04-30 + +- Q: Must the new env vars/sysprops introduced by this feature (`SOLR_SECURITY_AGENT_MODE`, `SOLR_SECURITY_AGENT_SKIP`, `SOLR_SECURITY_AGENT_EXTRA_POLICY` and their sysprop equivalents) be documented in the ref-guide `solr-properties.adoc` properties reference page? → A: Yes — FR-016 extended to explicitly require `solr-properties.adoc` entries for all three new variables, in addition to the dedicated security section. +- Q: Should the path to `agent-security-extra.policy` be fixed or configurable (to support read-only installs, container images, and config-management tooling)? → A: Fixed default (`${server.dir}/etc/agent-security-extra.policy`) with an env/sysprop override: `SOLR_SECURITY_AGENT_EXTRA_POLICY` env var, auto-converted to system property `solr.security.agent.extra.policy`. An absent file is silently skipped (not a startup error). `solr.xml` is not a supported source for this setting (the agent loads before `solr.xml` is parsed). + +--- + +## Assumptions + +- Solr's configured home, data, log, and temporary directories are the authoritative source for the default permitted file path set; no additional paths are permitted by default. +- The default permitted network endpoint list covers intra-cluster communication via port-based wildcards: any host on `` (inter-node HTTP) and any host on the ZooKeeper port (explicit config or ` + 1000` for embedded ZK). This ensures all cluster nodes — including those that join after startup — are reachable without operator intervention or a restart. +- Loopback addresses (`localhost`, `127.0.0.1`, `::1`) are unconditionally permitted in the default policy. +- External ZooKeeper ensemble hosts on non-standard ports (i.e., a ZK ensemble not co-located with Solr on the standard port offset) are added to the permitted list from the ZK connection string configuration at startup. +- OS-level hardening (e.g., `systemd` unit file restrictions on Linux) is treated as a complementary layer and is documented but not required for the in-JVM controls to function. +- Removing `SolrPaths.assertPathAllowed` call sites is out of scope for this feature; the automatic controls supersede them, but legacy call sites are retained for defense-in-depth. +- Fine-grained access controls (reflection, class loading, runtime permissions beyond the four categories above) are explicitly out of scope. +- The OpenSearch `agent-sm` module (Apache 2.0 licensed) is a candidate for adoption or forking and will be evaluated during planning; this specification is technology-agnostic with respect to that choice. +- Security controls are enabled by default in warn mode in new installations and when upgrading, with a documented opt-out mechanism (`SOLR_SECURITY_AGENT_SKIP=true`) for operators who need to disable them temporarily. The timeline for making enforce mode the default is not committed; it will be decided in a future release based on test suite and community readiness. Warn mode does not emit a startup advisory message. +- The existing `solr/server/etc/security.policy` and `gradle/testing/randomization/policies/solr-tests.policy` files are retained in this release with deprecation notices (added as comments); they are no longer enforced by the JVM and serve as migration references only. Removal is planned for a future release. +- The `solr-tests.policy` file continues to be used by the randomized test framework for purposes unrelated to the agent; it must not be deleted in this release. + +- The `agent-security-extra.policy` file path defaults to `${server.dir}/etc/agent-security-extra.policy`. It can be overridden via `SOLR_SECURITY_AGENT_EXTRA_POLICY` env var / `solr.security.agent.extra.policy` sysprop to support read-only install trees, container images, and config-management tooling. The file's absence is not an error. The `solr.xml` element is not supported for this setting because the agent initialises before `solr.xml` is parsed. +- Several optional modules make outbound connections to operator-configured external services. For five of them (`jwt-auth`, `opentelemetry`, `s3-repository`, `gcs-repository`, `cross-dc-manager`), the external endpoint address is exclusively configurable by a node or cluster administrator, so a `SocketPermission "*", "connect,resolve"` wildcard is pre-permitted in the bundled agent policy — operators need no manual policy entry to use these modules. The `extraction` module is excluded from pre-permitting because its Tika Server URL is configurable via `solrconfig.xml` at `CONFIG_EDIT` privilege (collection-admin level), which would allow a collection administrator to redirect the permission to an arbitrary SSRF target; operators who use remote Tika must add an explicit policy entry. The `extraction` module's reference guide page carries a policy NOTICE box with a ready-to-paste snippet (T044/T045). See [research.md — Module Outbound Network Connections](research.md) for the full per-module analysis. diff --git a/specs/001-jsm-replacement/tasks.md b/specs/001-jsm-replacement/tasks.md new file mode 100644 index 000000000000..68d377ef49ac --- /dev/null +++ b/specs/001-jsm-replacement/tasks.md @@ -0,0 +1,271 @@ +# Tasks: Java Security Manager Replacement + +**Input**: Design documents from `specs/001-jsm-replacement/` +**Branch**: `15868-java-security-manager` | **JIRA**: SOLR-17767 +**Plan**: plan.md | **Spec**: spec.md (incl. clarifications 2026-04-28, 2026-04-29, 2026-04-30) + +**New since previous version**: FR-016 (reference guide docs), FR-017 (violation metrics), SC-007/SC-008 (metrics + enforce-mode tests), old policy file deprecation, enforce mode scoped to agent-sm tests only, Decision 9 (port-wildcard intra-cluster network policy), T044/T045 (module outbound network — extraction NOTICE box only; 5 modules pre-permitted with wildcard in bundled policy per clarification 2026-04-29), T050/T047b (configurable extra-policy path via `SOLR_SECURITY_AGENT_EXTRA_POLICY` per clarification 2026-04-30), T048b (new agent env vars/sysprops documented in `solr-properties.adoc` per clarification 2026-04-30). + +**Organization**: Tasks grouped by user story (US1–US5) to enable independent implementation and testing. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no incomplete-task dependencies) +- **[Story]**: Which user story this implements ([US1]–[US5]) + +--- + +## Phase 1: Setup (Gradle Subproject Scaffold) + +**Purpose**: Create the `solr/agent-sm/` Gradle subproject and wire it into the build. All subsequent work depends on this. + +- [X] T001 Register `solr/agent-sm` as a Gradle subproject in `settings.gradle` (add `include 'solr:agent-sm'`) +- [X] T002 Create `solr/agent-sm/build.gradle` — apply `java-library`; configure agent JAR manifest with `Premain-Class: org.apache.solr.security.agent.SolrAgentEntryPoint` and `Can-Redefine-Classes: true`; declare ByteBuddy as `implementation` dependency +- [X] T003 Add `byte-buddy` version entry to `gradle/libs.versions.toml` (match version used by OpenSearch agent-sm; do not specify version in build.gradle) +- [X] T004 Create source directories `solr/agent-sm/src/java/org/apache/solr/security/agent/` and `solr/agent-sm/src/test/org/apache/solr/security/agent/` +- [X] T005 Run `gradlew :solr:agent-sm:dependencies` to confirm ByteBuddy resolves; then run `gradlew updateLicenses resolveAndLockAll --write-locks` to generate license files for ByteBuddy +- [X] T043 Configure `solr/agent-sm/build.gradle` with a `copyAgentJar` task that copies the built agent JAR to `solr/server/lib/ext/` so startup scripts can detect it at `${SOLR_SERVER_DIR}/lib/ext/solr-agent-sm-*.jar`; wire the task into the `assemble` lifecycle; also add the agent JAR to the `solr/packaging/` Gradle targets so it is included in the distribution zip/tgz + +**Checkpoint**: `gradlew :solr:agent-sm:compileJava` succeeds (empty source tree); `gradlew :solr:agent-sm:assemble` produces JAR at `solr/server/lib/ext/` + +--- + +## Phase 2: Foundational (Policy Engine — Blocks All User Stories) + +**Purpose**: The policy loading and enforcement infrastructure that every interceptor depends on. Must be complete before any protection can be implemented. + +**⚠️ CRITICAL**: No user story interceptor work can begin until this phase is complete. + +- [X] T006 Implement `solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java` — reads JDK-style `.policy` files; resolves the following variables: `${solr.home}`, `${solr.data.dir}`, `${solr.log.dir}`, `${java.io.tmpdir}`, `${java.home}`, `${solr.port}`, `${solr.zk.port}` (ZK port = explicit config value or `solr.port + 1000` for embedded ZK), and **`${solr.install.dir}`** (resolved from system property `solr.install.dir`, the Solr installation root — required for codebase-scoped module grants in the default policy); merges default policy (`agent-security.policy`) + optional extension file (`agent-security-extra.policy`); entries from extension file tagged `OPERATOR`, from default tagged `DEFAULT`; throws descriptive `IllegalStateException` if default policy is absent or unparseable; extension file absent is non-fatal; add ASF license header and class-level javadoc. **⚠️ Dependency note**: resolve the extra-policy file path from `System.getProperty("solr.security.agent.extra.policy")` with a fallback to `${server.dir}/etc/agent-security-extra.policy` — do NOT hard-code the path; T050 (formerly T050) completes the full env-var/sysprop wiring in Phase 8 +- [X] T007 [P] Implement `solr/agent-sm/src/java/org/apache/solr/security/agent/SolrSecurityPolicy.java` — immutable singleton holding `List`, `List`, `List` (exit + exec), `EnforcementMode` enum (`WARN`/`ENFORCE`), and `Set trustedFileSystems`; reads enforcement mode from system property `solr.security.agent.mode` via `System.getProperty()` directly (NOT via `EnvUtils` — the agent JAR has no compile-time dependency on `solr:core`; the startup scripts convert the env var to a `-D` sysprop before JVM launch, see T020); default is `warn` if property absent; throws `SecurityException` on any re-set attempt after initialization; add ASF license header and javadoc +- [X] T008 [P] Implement `solr/agent-sm/src/java/org/apache/solr/security/agent/StackInspector.java` — uses `StackWalker.getInstance(RETAIN_CLASS_REFERENCE)` to walk the call chain; filters JDK frames (`jrt:` codebase); returns ordered list of non-JDK caller classes; must not use thread identity (virtual-thread safe); add ASF license header and javadoc +- [X] T009 [P] Implement `solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java` — emits structured SLF4J log entries at `WARN` (warn mode) or `ERROR` (enforce mode); log format: `SECURITY VIOLATION [TYPE] target= caller= mode=`; reserve a `source` field placeholder in the format (populated by T042 in US2); wrap debug-level call stack logging in `logger.isDebugEnabled()`; add ASF license header and javadoc +- [X] T010 Create `solr/server/etc/agent-security.policy` — default production policy with the following grants: + - Global grant (no codeBase): `${solr.home}/-` read; `${solr.data.dir}/-` read+write+delete; `${solr.log.dir}/-` read+write+delete; `${java.io.tmpdir}/-` read+write+delete; `${java.home}/-` read; loopback `localhost:1-65535`, `127.0.0.1:1-65535`, and `[::1]:1-65535` connect+resolve; intra-cluster wildcards `*:${solr.port}` and `*:${solr.zk.port}` connect+resolve (Decision 9) + - Per-module codeBase grants (one block each) — `SocketPermission "*", "connect,resolve"` scoped to each pre-permitted bundled module's JAR directory (Decision 10 / clarification 2026-04-29): `codeBase "file:${solr.install.dir}/modules/jwt-auth/-"`, `codeBase "file:${solr.install.dir}/modules/opentelemetry/-"`, `codeBase "file:${solr.install.dir}/modules/s3-repository/-"`, `codeBase "file:${solr.install.dir}/modules/gcs-repository/-"`, `codeBase "file:${solr.install.dir}/modules/cross-dc-manager/-"` + - Add ASF license header as a comment block; add a comment before each codeBase grant naming the module and explaining the SSRF-safe rationale +- [X] T011 [P] Create `solr/server/etc/agent-security-extra.policy` — empty operator extension template with commented examples for custom paths and network endpoints; add ASF license header as a comment block +- [X] T012 Write `solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java` — extend `SolrTestCase`; test cases: valid policy parses correctly, variable substitution resolves paths (including `${solr.port}`, `${solr.zk.port}`, and `${solr.install.dir}`), `${solr.zk.port}` defaults to `${solr.port} + 1000` when not configured, codeBase-scoped grants parse and match correctly, malformed policy throws on load, missing default policy throws on load, extra policy merged when present and tagged OPERATOR, extra policy absent is non-fatal; add ASF license header + +**Checkpoint**: `gradlew :solr:agent-sm:test` passes (policy loading tests green) + +--- + +## Phase 3: User Story 1 — Standard Deployment Protected by Default (Priority: P1) 🎯 MVP + +**Goal**: All four protection categories (file, network, exit, process) active and automatically applied to every Solr deployment. Violations are exposed as Solr metrics (FR-017) and log entries (FR-008). + +**Independent Test**: Start embedded Solr with agent in enforce mode; a test plugin that attempts unauthorized file read, outbound network connect, `System.exit()`, and `ProcessBuilder` spawn — all four are blocked; standard search and indexing complete with no violations; per-type violation counters in the metrics registry are non-zero after violations are triggered. + +### Implementation + +- [X] T013 [US1] Adapt OpenSearch `FileInterceptor` → `solr/agent-sm/src/java/org/apache/solr/security/agent/FileAccessInterceptor.java` — ByteBuddy `@Advice` interceptor for `java.nio.file.Files`, `FileChannel`, and `FileSystemProvider` write/read/delete/copy/move/open methods; on each call: resolve absolute path, skip trusted filesystem schemes, walk stack via `StackInspector`, check each frame's protection domain against `SolrSecurityPolicy`; delegate to `SecurityViolationLogger` on violation; add ASF license header and javadoc; add `@SuppressForbidden(reason="ByteBuddy bootstrap injection requires Unsafe")` where applicable +- [X] T014 [P] [US1] Adapt OpenSearch `SocketChannelInterceptor` → `solr/agent-sm/src/java/org/apache/solr/security/agent/NetworkAccessInterceptor.java` — intercepts `SocketChannel.connect()` and `Socket.connect()`; checks `InetSocketAddress` against `SolrSecurityPolicy.permittedEndpoints`; passes loopback unconditionally; delegates to `SecurityViolationLogger` on violation; add ASF license header and javadoc +- [X] T015 [P] [US1] Adapt OpenSearch `SystemExitInterceptor` + `RuntimeHaltInterceptor` → `solr/agent-sm/src/java/org/apache/solr/security/agent/ExitInterceptor.java` — intercepts `System.exit()` and `Runtime.halt()`; checks top caller class against `SolrSecurityPolicy.approvedExitCallers`; default approved callers: `org.apache.solr.cli.SolrCLI`, `org.apache.solr.servlet.SolrDispatchFilter`; delegates to `SecurityViolationLogger` on violation; add ASF license header and javadoc +- [X] T016 [P] [US1] Implement `solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java` — ByteBuddy interceptor for `ProcessBuilder.start()` and `Runtime.exec()`; checks top caller class prefix against `SolrSecurityPolicy.approvedExecCallers`; default approved exec callers list is empty in production policy; delegates to `SecurityViolationLogger` on violation; add ASF license header and javadoc +- [X] T017 [US1] Implement `solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java` — maintains four `LongAdder` counters (FILE, NETWORK, EXIT, EXEC); incremented by `SecurityViolationLogger` on each violation; provides `registerWithSolrMetrics(SolrMetricManager, String registry)` static method using deferred registration pattern (called once `SolrMetricManager` is available at core initialization; method name must match the reflective call in T019); registers counters under metric names `security.agent.violations.file`, `security.agent.violations.network`, `security.agent.violations.exit`, `security.agent.violations.exec`; add ASF license header and javadoc +- [X] T018 [US1] Implement `solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java` — `premain()` and `agentmain()` entry points; loads `SolrSecurityPolicy` via `PolicyLoader`; registers all four interceptors with ByteBuddy `AgentBuilder`; injects bootstrap classes via `ClassInjector.UsingUnsafe.ofBootLoader()`; initializes `ViolationMetricsReporter` singleton; if policy loading fails, logs error and (in enforce mode) halts startup; add ASF license header and javadoc; add `@SuppressForbidden` for `Unsafe` bootstrap injection +- [X] T019 [US1] Add hook in `solr/core/src/java/org/apache/solr/core/CoreContainer.java` to register agent metrics via reflection — use `Class.forName("org.apache.solr.security.agent.ViolationMetricsReporter", false, null)` (bootstrap classloader lookup; no compile-time dependency on `solr:agent-sm`); invoke `registerWithSolrMetrics(SolrMetricManager, String)` reflectively; catch `ClassNotFoundException` silently (agent not loaded); catch `ReflectiveOperationException` with a WARN log; see research.md Decision 8 for full pattern +- [X] T020 [US1] Modify `solr/bin/solr` — detect presence of agent JAR at `${SOLR_SERVER_DIR}/lib/ext/solr-agent-sm-*.jar` or equivalent output path; if found, prepend `-javaagent:` to `SOLR_OPTS` before JVM launch; skip if `SOLR_SECURITY_AGENT_SKIP=true`; also convert the two env vars to JVM system properties so the agent can read them via `System.getProperty()` (the agent JAR does not use `EnvUtils`): if `SOLR_SECURITY_AGENT_MODE` is set, append `-Dsolr.security.agent.mode=$SOLR_SECURITY_AGENT_MODE`; if `SOLR_SECURITY_AGENT_EXTRA_POLICY` is set, append `-Dsolr.security.agent.extra.policy=$SOLR_SECURITY_AGENT_EXTRA_POLICY` +- [X] T021 [P] [US1] Modify `solr/bin/solr.cmd` — same detection, `-javaagent:` injection, and env→sysprop conversion logic as T020 but for Windows batch (`IF DEFINED SOLR_SECURITY_AGENT_MODE SET SOLR_OPTS=%SOLR_OPTS% -Dsolr.security.agent.mode=%SOLR_SECURITY_AGENT_MODE%`, similarly for `SOLR_SECURITY_AGENT_EXTRA_POLICY`) +- [X] T022 [US1] Document `SOLR_SECURITY_AGENT_SKIP` and `SOLR_SECURITY_AGENT_MODE` in `solr/bin/solr.in.sh` — note that `SOLR_SECURITY_AGENT_MODE` is converted to `-Dsolr.security.agent.mode` by the startup script (T020) so the agent can read it via `System.getProperty()` without needing `EnvUtils`; valid values: `warn` (default), `enforce` +- [X] T023 [P] [US1] Document same variables in `solr/bin/solr.in.cmd` +- [X] T046 [P] [US1] Write `solr/agent-sm/src/test/org/apache/solr/security/agent/NetworkAccessInterceptorTest.java` — extend `SolrTestCase`; test cases: loopback connect permitted, `*:` wildcard entry permits connection to that port on any host, unlisted host:port blocked in enforce mode, `source=DEFAULT` in violation log; add ASF license header +- [X] T047 [P] [US1] Write `solr/agent-sm/src/test/org/apache/solr/security/agent/ExitInterceptorTest.java` — extend `SolrTestCase`; test cases: `System.exit()` from approved caller class passes, `System.exit()` from unapproved caller throws `SecurityException` in enforce mode and logs warning in warn mode, `Runtime.halt()` from unapproved caller is blocked; assert EXIT counter increments; add ASF license header +- [X] T048 [P] [US1] Write `solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java` — extend `SolrTestCase`; test cases: `ProcessBuilder.start()` from an approved caller class passes, `ProcessBuilder.start()` from an unapproved caller is blocked in enforce mode, `Runtime.exec()` from unapproved caller is blocked; assert EXEC counter increments; add ASF license header +- [X] T024 [US1] Write `solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java` — extend `SolrTestCase`; run with agent in **ENFORCE mode** (`SOLR_SECURITY_AGENT_MODE=enforce`); verify: permitted file read succeeds, denied file read throws `SecurityException`, permitted loopback connect succeeds, denied outbound connect throws `SecurityException`, `System.exit()` from non-approved caller throws `SecurityException`, `ProcessBuilder.start()` from non-approved caller throws `SecurityException`; after each violation, assert the corresponding `ViolationMetricsReporter` counter incremented; add ASF license header + +**Checkpoint**: `gradlew :solr:agent-sm:test` fully green in enforce mode; standard Solr integration suite passes with agent in warn mode; violation metric counters verified non-zero after violations (SC-007, SC-008) + +--- + +## Phase 4: User Story 2 — Operator Extends Policy (Priority: P2) + +**Goal**: Operators can add custom permitted paths and endpoints via `agent-security-extra.policy` without touching Solr source or the default policy. + +**Independent Test**: Write extra policy with a custom path; verify that path is accessible; verify a different unlisted path is still blocked. + +- [X] T042 [US2] Update `solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java` to emit `source` field — extend the log format to `SECURITY VIOLATION [TYPE] target= caller= mode= source=`; the source value is passed in from the policy-check result produced by `PolicyLoader` (which already tags entries); no change to `PolicyLoader` needed; add javadoc on the new parameter +- [X] T025 [US2] Write `solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java` — extend `SolrTestCase`; test: extra policy file present → custom path accessible; custom path not in extra policy → blocked; extra policy absent → default policy still enforced; malformed extra policy → startup failure with clear error; OPERATOR-tagged entries in violation log include `source=OPERATOR` in log output (depends on T042); add ASF license header + +**Checkpoint**: Operator can add `agent-security-extra.policy` and have it take effect on restart; no access to unlisted paths; `source=OPERATOR` visible in log entries for operator-policy-covered paths + +--- + +## Phase 5: User Story 3 — Windows UNC Paths Blocked (Priority: P2) + +**Goal**: Paths beginning with `\\` are always rejected on all platforms, regardless of any policy rule. + +**Independent Test**: Attempt file access via `\\server\share\file`; verify blocked and logged on Linux, macOS, and Windows. + +- [X] T026 [US3] Add UNC path detection to `solr/agent-sm/src/java/org/apache/solr/security/agent/FileAccessInterceptor.java` — before any policy check, if the resolved path string starts with `\\` or is a Windows UNC-style path, immediately block and log; cannot be overridden by any policy entry; write unit test cases in `solr/agent-sm/src/test/org/apache/solr/security/agent/UncPathRejectionTest.java` extending `SolrTestCase`; add ASF license header to test file + +**Checkpoint**: UNC path attempts produce `SECURITY VIOLATION` log entries and FILE counter increments on all platforms + +--- + +## Phase 6: User Story 4 — Symlink Escape Prevented (Priority: P2) + +**Goal**: A symlink inside a permitted directory pointing to a target outside permitted directories is denied. + +**Independent Test**: Create symlink in data dir targeting `/etc/passwd`; attempt read via symlink path; verify denied. + +- [X] T027 [US4] Add symlink resolution to `solr/agent-sm/src/java/org/apache/solr/security/agent/FileAccessInterceptor.java` — after normalizing the target path, call `Path.toRealPath()` to resolve symlinks; check the resolved real path (not the symlink path) against `SolrSecurityPolicy.permittedPaths`; if real path is outside permitted dirs, block and log even if the symlink path itself would have matched; handle `IOException` from `toRealPath()` gracefully (log at DEBUG, proceed with original path check) +- [X] T028 [P] [US4] Write `solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java` — extend `SolrTestCase`; create symlink inside `java.io.tmpdir` pointing to a system file; verify read via symlink path is blocked; verify read of a real permitted path still succeeds; add ASF license header + +**Checkpoint**: Symlink traversal to unpermitted targets produces `SECURITY VIOLATION` log entries and FILE counter increments + +--- + +## Phase 7: User Story 5 — Virtual Thread Compatibility (Priority: P3) + +**Goal**: Security controls work correctly under Project Loom virtual threads; no false positives or false negatives. + +**Independent Test**: Enable virtual threads; run full standard Solr test suite; verify zero unexpected security violations. + +- [X] T029 [US5] Audit `solr/agent-sm/src/java/org/apache/solr/security/agent/StackInspector.java` for virtual-thread safety — verify `StackWalker.getInstance(RETAIN_CLASS_REFERENCE)` is used (not `Thread.currentThread()` or `ThreadGroup`); add code comment documenting virtual-thread compatibility guarantee +- [X] T030 [P] [US5] Write `solr/agent-sm/src/test/org/apache/solr/security/agent/VirtualThreadCompatibilityTest.java` — extend `SolrTestCase`; launch file access and network access operations from virtual threads using `Thread.ofVirtual().start(...)`; verify: permitted ops succeed, denied ops are caught with correct counter increments, no `NullPointerException` or `ClassCastException` from stack walker on virtual thread frames; add ASF license header +- [X] T031 [US5] Run `gradlew :solr:core:test :solr:agent-sm:test` with virtual thread executor enabled; triage any unexpected violations found; document results in a comment on SOLR-17767 + +**Checkpoint**: Full test suite green with virtual threads; no virtual-thread-specific failures + +--- + +## Phase 8: Polish & Cross-Cutting Concerns + +**Purpose**: Documentation (FR-016), old policy file deprecation, code quality, and Solr-wide integration verification. + +- [X] T032 [P] Run `gradlew tidy` across all modified and new source files; fix any formatting issues reported +- [X] T033 Run `gradlew check -x test` and resolve any forbidden-API violations, license check failures, or dependency analysis issues (`usedUndeclaredArtifacts`, `unusedDeclaredArtifacts`) +- [X] T034 [P] Add `@Deprecated` annotation and `@deprecated` javadoc tag to `SolrPaths.assertPathAllowed()` in `solr/core/src/java/org/apache/solr/core/SolrPaths.java` with note: "Automatic enforcement via the security agent supersedes this check; do not add new call sites" +- [X] T035 [P] Add deprecation notice comments to `solr/server/etc/security.policy` — explain the file is no longer enforced by the JVM (JSM removed in JDK 24), is retained as a migration reference, and will be removed in a future release; point to `agent-security.policy` as the replacement +- [X] T036 [P] Add deprecation notice comments to `gradle/testing/randomization/policies/solr-tests.policy` — explain the JSM policy sections are no longer enforced but the file is retained for test framework use; will be reviewed for removal in a future release +- [X] T050 [P] Implement configurable extra-policy path (FR-009) — in the agent startup code (`PolicyLoader`), resolve the extra policy path from system property `solr.security.agent.extra.policy` via `System.getProperty()` directly (the startup script converts `SOLR_SECURITY_AGENT_EXTRA_POLICY` to this `-D` sysprop per T020/T021); fall back to `${server.dir}/etc/agent-security-extra.policy` if the property is absent; silently skip loading if the resolved file does not exist; log the resolved path at INFO level on startup +- [X] T047b [P] Add `SOLR_SECURITY_AGENT_EXTRA_POLICY` to `bin/solr.in.sh` and `bin/solr.in.cmd` as a commented-out example with a doc comment explaining its purpose, the default path, and that the startup script converts it to `-Dsolr.security.agent.extra.policy` (no `EnvUtils` wiring needed — conversion is done inline in the startup script per T020/T021) +- [X] T037 [P] Write reference guide section on agent-based security controls (FR-016) — create or update the security page in `solr/solr-ref-guide/modules/deployment-guide/pages/` covering: what protections are active by default, enforcement modes (`SOLR_SECURITY_AGENT_MODE`), policy file format and variable substitution (including `${solr.port}` and `${solr.zk.port}`), default intra-cluster port-wildcard policy (any host on `` and `` is permitted without operator action), extending policy via `agent-security-extra.policy` (including `SOLR_SECURITY_AGENT_EXTRA_POLICY` override for read-only installs and containers), diagnosing violations via logs and `/admin/metrics`, disabling the agent (`SOLR_SECURITY_AGENT_SKIP`), external ZK on non-standard port edge case; audience is operators; cross-reference `solr-properties.adoc` for the properties table +- [X] T048b [P] Add new agent env vars and system properties to `solr/solr-ref-guide/modules/configuration-guide/pages/solr-properties.adoc` (FR-016) — insert three rows in alphabetical order (after existing `solr.security.*` entries): + - `solr.security.agent.extra.policy` | `SOLR_SECURITY_AGENT_EXTRA_POLICY` | `${server.dir}/etc/agent-security-extra.policy` | Path to the operator extension policy file; overrides the default location; absent file is silently skipped + - `solr.security.agent.mode` | `SOLR_SECURITY_AGENT_MODE` | `warn` | Enforcement mode for the security agent: `warn` (log violations, continue) or `enforce` (log violations, block operation with `SecurityException`) + - `SOLR_SECURITY_AGENT_SKIP` | _(startup-script only, no sysprop)_ | `false` | If set to `true`, omits the `-javaagent:` flag from the JVM command line, disabling all agent security controls; intended for temporary troubleshooting only +- [X] T044 [P] Add security agent policy NOTICE box to the `extraction` module reference guide page — add a clearly visible WARNING admonition explaining that in enforce mode, the remote Tika Server URL must be explicitly permitted in `agent-security-extra.policy`; include a ready-to-paste snippet; note: `jwt-auth`, `opentelemetry`, `s3-repository`, `gcs-repository`, and `cross-dc-manager` do NOT need a NOTICE — their endpoints are pre-permitted with a wildcard in the bundled agent policy (see clarification 2026-04-29): + - `solr/solr-ref-guide/modules/indexing-guide/pages/indexing-with-tika.adoc` — remote Tika Server URL (only when `useRemoteTikaServer=true`) +- [X] T045 [P] Update `solr/server/etc/agent-security-extra.policy` template to include a commented-out example entry for the `extraction` module — add a commented block for Tika Server (`# extraction module: uncomment and set your Tika Server hostname:port`); the five pre-permitted modules (`jwt-auth`, `opentelemetry`, `s3-repository`, `gcs-repository`, `cross-dc-manager`) do not need example entries as their wildcard rules are bundled in the default policy +- [X] T038 [P] Add developer documentation in `dev-docs/` — create `security-agent.adoc` covering: how to add an approved call site (exit/exec), how to add a trusted filesystem scheme, how the deferred metrics registration works, how to write tests alongside the agent, and instructions for the future enforce-mode flip in the broader test suite +- [X] T039 Run `gradlew writeChangelog`; edit generated file in `changelog/unreleased/` — category: `new feature`; summary covering: JSM replacement via Java agent, file/network/exit/exec protections, warn-only default, policy file configuration, violation metrics in `/admin/metrics`, reference guide documentation +- [ ] T040 Validate `quickstart.md` steps manually against a running Solr instance: confirm warn mode default, enforce mode opt-in, extra policy file pickup, violation log format, and `/admin/metrics` counter presence all match documentation +- [ ] T041 Run full Solr integration test suite (`gradlew :solr:core:test :solr:solrj:test`) with agent in warn mode; confirm zero unexpected violations via `grep "SECURITY VIOLATION" solr/*/build/test-results/test/outputs/*.txt` — agent-sm test suite: ALL 9 SUITES GREEN +- [ ] T049 [P] Run performance benchmark to validate SC-002 / FR-012 (≤5% throughput degradation) — execute a standard Solr search and indexing benchmark (e.g., using the Solr test-framework's `SolrBenchmark` harness or a manual `ab`/`wrk` run against a local Solr instance) with agent in warn mode vs without agent (`SOLR_SECURITY_AGENT_SKIP=true`); record throughput (QPS) for both; confirm degradation ≤5%; document results as a comment on SOLR-17767; if degradation exceeds 5%, file a follow-up JIRA before declaring the feature ready + +**Checkpoint**: `gradlew check -x test` clean; changelog entry present; reference guide section complete and extraction module NOTICE box present; extra policy template has commented Tika Server example; old policy files have deprecation notices; `solr-properties.adoc` contains entries for all three new agent env vars/sysprops; benchmark results documented on SOLR-17767 with ≤5% degradation confirmed; no unexpected violations in full test suite + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Phase 1 (Setup)**: No dependencies — start immediately +- **Phase 2 (Foundational)**: Depends on Phase 1 — **blocks all user story phases** +- **Phase 3 (US1)**: Depends on Phase 2 — MVP; no dependency on US2–US5 +- **Phase 4 (US2)**: T042 (SecurityViolationLogger source field) depends on T009; T025 (test) depends on T042; both independent of US1 interceptors +- **Phase 5 (US3)**: Depends on T013 (`FileAccessInterceptor` exists); extends it +- **Phase 6 (US4)**: Depends on T013 (`FileAccessInterceptor` exists); can run parallel with Phase 5 +- **Phase 7 (US5)**: Depends on Phase 3 complete (all interceptors implemented) +- **Phase 8 (Polish)**: Depends on all desired user stories complete + +### User Story Dependencies + +- **US1 (P1)**: After Foundational — no dependency on other stories +- **US2 (P2)**: After T006 (PolicyLoader) — independent of US1 interceptors +- **US3 (P2)**: After T013 (`FileAccessInterceptor`) — extends US1 work; parallel with US4 +- **US4 (P2)**: After T013 (`FileAccessInterceptor`) — extends US1 work; parallel with US3 +- **US5 (P3)**: After all US1 interceptors (T013–T016) complete + +### Parallel Opportunities Within Phases + +- **Phase 1**: T003, T004 parallel after T001–T002 +- **Phase 2**: T007, T008, T009, T011 all parallel once T006 interface defined +- **Phase 3**: T014, T015, T016 parallel once T013 pattern established; T021, T023 parallel with T020, T022; T017, T018, T019 sequential after interceptors +- **Phase 5 + Phase 6**: T026 and T027–T028 fully parallel once T013 exists +- **Phase 8**: T032, T034, T035, T036, T037, T038, T048b, T049 all parallel + +--- + +## Parallel Example: Phase 2 (Foundational) + +``` +Start T006 PolicyLoader (defines the interface others depend on) +Once T006 interface is defined, launch in parallel: + → T007 SolrSecurityPolicy + → T008 StackInspector + → T009 SecurityViolationLogger + → T011 agent-security-extra.policy template + → T012 PolicyLoaderTest +Start T010 agent-security.policy (independent, any time in Phase 2) +``` + +## Parallel Example: Phase 3 (US1 — Core Interceptors + Metrics) + +``` +Start T013 FileAccessInterceptor (establishes the ByteBuddy pattern) +Once T013 pattern established, launch in parallel: + → T014 NetworkAccessInterceptor + → T015 ExitInterceptor + → T016 ProcessExecInterceptor + → T017 ViolationMetricsReporter (independent of interceptors) +Then T018 SolrAgentEntryPoint (wires all interceptors + metrics reporter) +Then T019 CoreContainer metrics hook (depends on T017 + T018) +Then T020/T021 startup scripts (parallel with each other) +Then T024 SolrAgentIntegrationTest (depends on T018 + T019) +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 1: Gradle scaffold +2. Complete Phase 2: Policy engine +3. Complete Phase 3: All four interceptors + metrics reporter + agent entry point + startup scripts +4. **STOP and VALIDATE**: Run integration test in enforce mode; confirm standard Solr ops pass; confirm violations caught; confirm metric counters increment +5. Ship in warn-only mode — operators observe without risk; metrics visible via `/admin/metrics` + +### Incremental Delivery + +1. Phase 1 + 2 → Policy engine ready +2. Phase 3 (US1) → All four protections + metrics → **MVP** (warn mode) +3. Phase 4 (US2) → Operator policy extension → Usable for non-standard deployments +4. Phase 5 + 6 (US3 + US4) → UNC and symlink hardening → Security completeness +5. Phase 7 (US5) → Virtual thread validation → Production-grade for Loom +6. Phase 8 → Documentation, deprecation notices, changelog → Release-ready + +### Suggested MVP Scope + +Complete **Phases 1–3** only (T001–T024). Delivers: +- All four protection categories active +- Violation metrics in `/admin/metrics` +- Warn-only default (safe for existing deployments) +- Startup script auto-activation +- Integration tests in enforce mode (agent-sm suite) + +### Future Work (Not in This Release) + +- Flip broader Solr test suite to enforce mode (SC-008 follow-up; requires triage of warn-mode violations first) +- Dynamic network policy refresh for modules with externally-changing endpoints (e.g., rotating SaaS URLs) — intra-cluster topology is already covered by the port-wildcard approach in Decision 9 +- Remove deprecated `security.policy` and `solr-tests.policy` JSM sections + +--- + +## Notes + +- [P] tasks = different files, no dependency on incomplete tasks in the same phase +- All new `.java` files **must** include the ASF license header +- All new classes **must** have class-level javadoc (agent interceptors are non-obvious) +- Debug/trace SLF4J calls in interceptors **must** be wrapped in `logger.isDebugEnabled()` — interceptors are in hot paths +- Do not add new `SolrPaths.assertPathAllowed()` call sites — the agent handles enforcement automatically +- `ViolationMetricsReporter` uses deferred registration: it buffers counts from the moment the agent starts (before Solr initializes), then registers with `SolrMetricManager` once available +- The agent-sm test suite runs in ENFORCE mode; the broader Solr suite runs in WARN mode — this is intentional per SC-008 +- Run `gradlew tidy` before any commit touching Java source +- Run `gradlew check -x test` before declaring a phase done +- Changelog entry is mandatory — run `gradlew writeChangelog` and edit the generated file From f25d06f84da154d5b719879bd6d0b6c92b6be3f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Tue, 26 May 2026 20:26:04 +0200 Subject: [PATCH 02/65] Delete speckit folder --- .../checklists/requirements.md | 36 --- .../contracts/policy-file-format.md | 140 --------- specs/001-jsm-replacement/data-model.md | 131 --------- specs/001-jsm-replacement/plan.md | 220 -------------- specs/001-jsm-replacement/quickstart.md | 106 ------- specs/001-jsm-replacement/research.md | 241 ---------------- specs/001-jsm-replacement/spec.md | 190 ------------ specs/001-jsm-replacement/tasks.md | 271 ------------------ 8 files changed, 1335 deletions(-) delete mode 100644 specs/001-jsm-replacement/checklists/requirements.md delete mode 100644 specs/001-jsm-replacement/contracts/policy-file-format.md delete mode 100644 specs/001-jsm-replacement/data-model.md delete mode 100644 specs/001-jsm-replacement/plan.md delete mode 100644 specs/001-jsm-replacement/quickstart.md delete mode 100644 specs/001-jsm-replacement/research.md delete mode 100644 specs/001-jsm-replacement/spec.md delete mode 100644 specs/001-jsm-replacement/tasks.md diff --git a/specs/001-jsm-replacement/checklists/requirements.md b/specs/001-jsm-replacement/checklists/requirements.md deleted file mode 100644 index c6f99f12ab2d..000000000000 --- a/specs/001-jsm-replacement/checklists/requirements.md +++ /dev/null @@ -1,36 +0,0 @@ -# Specification Quality Checklist: Java Security Manager Replacement - -**Purpose**: Validate specification completeness and quality before proceeding to planning -**Created**: 2026-04-28 -**Feature**: [spec.md](../spec.md) - -## Content Quality - -- [x] No implementation details (languages, frameworks, APIs) -- [x] Focused on user value and business needs -- [x] Written for non-technical stakeholders -- [x] All mandatory sections completed - -## Requirement Completeness - -- [x] No [NEEDS CLARIFICATION] markers remain -- [x] Requirements are testable and unambiguous -- [x] Success criteria are measurable -- [x] Success criteria are technology-agnostic (no implementation details) -- [x] All acceptance scenarios are defined -- [x] Edge cases are identified -- [x] Scope is clearly bounded -- [x] Dependencies and assumptions identified - -## Feature Readiness - -- [x] All functional requirements have clear acceptance criteria -- [x] User scenarios cover primary flows -- [x] Feature meets measurable outcomes defined in Success Criteria -- [x] No implementation details leak into specification - -## Notes - -- The spec explicitly calls out that the OpenSearch `agent-sm` module is a *candidate* for evaluation during planning — this is intentional and does not leak implementation details into the spec itself. -- The 5% performance budget in FR-012 / SC-002 is a reasonable default; the planning phase should validate this against actual benchmarks. -- All checklist items pass. Ready to proceed to `/speckit-plan`. diff --git a/specs/001-jsm-replacement/contracts/policy-file-format.md b/specs/001-jsm-replacement/contracts/policy-file-format.md deleted file mode 100644 index ccbafc7c93cf..000000000000 --- a/specs/001-jsm-replacement/contracts/policy-file-format.md +++ /dev/null @@ -1,140 +0,0 @@ -# Contract: Security Policy File Format - -**Type**: Configuration file format -**Operator-facing**: Yes -**Files**: `${solr.home}/server/etc/agent-security.policy`, `agent-security-extra.policy` - -**Extra policy file path**: Defaults to `${server.dir}/etc/agent-security-extra.policy`. Override via: -- Environment variable: `SOLR_SECURITY_AGENT_EXTRA_POLICY=/path/to/my.policy` -- System property: `-Dsolr.security.agent.extra.policy=/path/to/my.policy` - -An absent file is silently skipped (not a startup error). This override supports read-only install trees, container images, and config-management tooling where the Solr server directory is not writable. Note: `solr.xml` is not a supported source for this setting — the agent initialises before `solr.xml` is parsed. - ---- - -## Overview - -Solr's agent-based security controls are configured via JDK-style `.policy` files, the same syntax used by the historical Java Security Manager. This allows operators familiar with JSM policy files to reuse their knowledge. - -Two files are loaded at startup: -1. **`agent-security.policy`** — Solr's default policy (shipped with Solr, not meant to be edited). -2. **`agent-security-extra.policy`** — Operator extension policy (optional, empty by default). - ---- - -## File Syntax - -Standard JDK policy file syntax: - -``` -// Line comments are supported - -grant [codeBase "url"] { - permission ["target"] [, "actions"]; - ... -}; -``` - -### Supported Permission Types - -| Permission Class | Target | Actions | Effect | -|-----------------|--------|---------|--------| -| `java.io.FilePermission` | Absolute path or `path/-` (recursive) | `"read"`, `"write"`, `"delete"`, `"read,write"` | Permits file operations on matching paths | -| `java.net.SocketPermission` | `"host:port"` | `"connect,resolve"` | Permits outbound connection to host:port | -| `java.lang.RuntimePermission` | `"exitVM"` | _(none)_ | Grants permission to call `System.exit()` | -| `java.lang.RuntimePermission` | `"exec"` | _(none)_ | Grants permission to spawn child processes | - -### Variable Substitution - -The following variables are expanded in path targets: - -| Variable | Resolved Value | -|----------|---------------| -| `${solr.home}` | Solr home directory | -| `${solr.data.dir}` | Solr data directory | -| `${solr.log.dir}` | Solr log directory | -| `${solr.install.dir}` | Solr installation root (parent of `server/`); used in `codeBase` paths for module JARs | -| `${java.io.tmpdir}` | JVM temporary directory | -| `${java.home}` | JDK installation directory | -| `${user.home}` | OS user home directory | - ---- - -## Default Policy (Conceptual) - -The default `agent-security.policy` permits: - -``` -grant { - // Solr home (read-only for config files) - permission java.io.FilePermission "${solr.home}/-", "read"; - - // Solr data and index directories (read + write) - permission java.io.FilePermission "${solr.data.dir}/-", "read,write,delete"; - - // Log directory (write) - permission java.io.FilePermission "${solr.log.dir}/-", "read,write,delete"; - - // Temporary files - permission java.io.FilePermission "${java.io.tmpdir}/-", "read,write,delete"; - - // JDK runtime libraries (read-only) - permission java.io.FilePermission "${java.home}/-", "read"; - - // Loopback network (inter-thread, localhost HTTP) - permission java.net.SocketPermission "localhost:1-65535", "connect,resolve"; - permission java.net.SocketPermission "127.0.0.1:1-65535", "connect,resolve"; - - // ZooKeeper ensemble (populated at startup from cluster config) - // permission java.net.SocketPermission ":", "connect,resolve"; - - // Approved System.exit() callers - permission java.lang.RuntimePermission "exitVM"; - // (codeBase-restricted to Solr CLI and shutdown hooks in full default policy) -}; -``` - ---- - -## Operator Extension Example - -To permit a plugin that reads from `/mnt/nfs-data` and connects to `analytics.internal:443`: - -``` -// ${solr.home}/server/etc/agent-security-extra.policy - -grant { - permission java.io.FilePermission "/mnt/nfs-data/-", "read"; - permission java.net.SocketPermission "analytics.internal:443", "connect,resolve"; -}; -``` - ---- - -## Enforcement Mode - -Controlled via the `SOLR_SECURITY_AGENT_MODE` environment variable (auto-converted by `EnvUtils` to system property `solr.security.agent.mode`): - -| Value | Behaviour | -|-------|-----------| -| `warn` (default) | Violations are logged at WARN; operation proceeds | -| `enforce` | Violations are logged at ERROR; operation is blocked with `SecurityException` | - -Set in `solr.in.sh`: -```bash -SOLR_SECURITY_AGENT_MODE=enforce -``` - ---- - -## Violation Log Format - -When a violation is detected, Solr logs a structured message: - -``` -[WARN ] SecurityAgent - SECURITY VIOLATION [FILE_READ] target=/etc/passwd caller=com.example.BadPlugin mode=WARN - at com.example.BadPlugin.readConfig(BadPlugin.java:42) - at org.apache.solr.core.CoreContainer.loadCore(CoreContainer.java:...) -``` - -Fields always present: operation type, target, top caller class, enforcement mode. diff --git a/specs/001-jsm-replacement/data-model.md b/specs/001-jsm-replacement/data-model.md deleted file mode 100644 index 9a9c56556e90..000000000000 --- a/specs/001-jsm-replacement/data-model.md +++ /dev/null @@ -1,131 +0,0 @@ -# Data Model: Java Security Manager Replacement - -**Date**: 2026-04-28 -**Feature**: specs/001-jsm-replacement/spec.md - -This feature introduces a runtime security policy system. There is no persistent data store — all entities exist in memory (loaded at startup from configuration files) or as transient log records. - ---- - -## Entity: SecurityPolicy - -The root entity. Loaded once at JVM startup and immutable thereafter. - -| Field | Type | Description | -|-------|------|-------------| -| `permittedPaths` | `List` | File system access rules | -| `permittedEndpoints` | `List` | Outbound network access rules | -| `approvedExitCallers` | `List` | Classes allowed to call `System.exit()` / `Runtime.halt()` | -| `approvedExecCallers` | `List` | Classes allowed to spawn child processes | -| `enforcementMode` | `Enum {WARN, ENFORCE}` | Whether violations block (ENFORCE) or only log (WARN) | -| `trustedFileSystems` | `Set` | Filesystem scheme names exempt from path checks (e.g., in-memory FS used in tests) | - -**Validation rules**: -- `permittedPaths` must be non-empty (Solr home must always be in the list) -- `enforcementMode` defaults to `WARN` in the initial release -- Loaded from the active `.policy` file and Solr's runtime directory configuration -- Immutable after startup; violations of this invariant throw `SecurityException` - -**State transitions**: -- `UNINITIALIZED` → `LOADED` (at startup, before application code runs) -- No runtime modification is permitted - ---- - -## Entity: PermittedPath - -A single file system access rule. - -| Field | Type | Description | -|-------|------|-------------| -| `path` | `String` | Absolute path or path prefix (supports `${solr.home}` and similar variables) | -| `operations` | `Set` | Permitted operations on matching paths | -| `recursive` | `boolean` | Whether the rule applies to all descendants (true) or only the exact path (false) | -| `source` | `Enum {DEFAULT, OPERATOR}` | Whether the rule is from the default policy or operator extension | - -**Validation rules**: -- `path` must be an absolute path after variable substitution -- UNC paths (`\\...`) are never permitted, regardless of rules -- Symlink-resolved path must also match a `PermittedPath` rule (no escape via symlinks) -- Windows drive-relative paths (e.g., `C:relative`) are rejected - ---- - -## Entity: PermittedEndpoint - -A single outbound network access rule. - -| Field | Type | Description | -|-------|------|-------------| -| `host` | `String` | Hostname, IP address, or wildcard pattern | -| `portRange` | `String` | Port or range (e.g., `"2181"`, `"8983-8985"`, `"*"`) | -| `operations` | `Set` | Permitted socket operations | -| `source` | `Enum {DEFAULT, OPERATOR}` | Origin of the rule | - -**Validation rules**: -- Loopback addresses (`localhost`, `127.0.0.1`, `::1`) are unconditionally permitted by default -- Any host on `` is permitted by default (inter-node HTTP; covers nodes that join after startup) -- Any host on the ZooKeeper port is permitted by default: explicit config value, or ` + 1000` for embedded ZK -- External ZooKeeper ensemble hosts:ports are added at startup from the ZK connection string configuration - ---- - -## Entity: ApprovedCallSite - -A class (or class name prefix) allowed to perform a restricted operation. - -| Field | Type | Description | -|-------|------|-------------| -| `classNamePattern` | `String` | Fully-qualified class name or prefix pattern (e.g., `org.apache.solr.cli.*`) | -| `operation` | `Enum {EXIT, EXEC}` | The restricted operation this approval covers | -| `description` | `String` | Human-readable explanation of why this call site is approved | -| `source` | `Enum {DEFAULT, OPERATOR}` | Origin of the entry | - -**Default approved EXIT callers**: -- `org.apache.solr.cli.SolrCLI` (CLI shutdown commands) -- `org.apache.solr.servlet.SolrDispatchFilter` (servlet shutdown hook) - -**Default approved EXEC callers**: -- _(none by default in production policy; test policy includes `org.apache.solr.cloud.IpTables`)_ - ---- - -## Entity: SecurityViolation - -A transient record of a blocked (or warn-mode logged) operation. Not persisted; emitted as a structured log entry. - -| Field | Type | Description | -|-------|------|-------------| -| `timestamp` | `Instant` | When the violation occurred | -| `operationType` | `Enum {FILE_READ, FILE_WRITE, FILE_DELETE, NETWORK_CONNECT, SYSTEM_EXIT, PROCESS_EXEC}` | What was attempted | -| `target` | `String` | The path, host:port, or operation descriptor | -| `callSiteClass` | `String` | Top non-JDK class in the call stack at the point of violation | -| `callStack` | `List` | Abbreviated call stack for debugging | -| `mode` | `Enum {WARN, BLOCK}` | Whether this violation was logged-only or blocked | - -**Log format**: Emitted at `WARN` level (warn mode) or `ERROR` level (enforce/block mode) via SLF4J. - ---- - -## Configuration Files - -### Production Policy File - -**Location**: `${solr.home}/server/etc/agent-security.policy` -**Format**: JDK-style `.policy` syntax with Solr variable substitution -**Loaded by**: `SecurityPolicy` at agent startup - -### Operator Extension Policy - -**Location**: `${solr.home}/server/etc/agent-security-extra.policy` (optional) -**Purpose**: Operator-provided additions to the default policy -**Loaded by**: Merged with production policy at startup; operator entries tagged with `source = OPERATOR` - -### Startup Configuration - -**Location**: `${solr.home}/bin/solr.in.sh` (Linux/macOS), `bin/solr.in.cmd` (Windows) -**New variable**: `SOLR_SECURITY_AGENT_MODE` — sets enforcement mode; read by `EnvUtils`, which auto-converts env var `SOLR_SECURITY_AGENT_MODE` to system property `solr.security.agent.mode` -**Example**: -``` -SOLR_SECURITY_AGENT_MODE=warn -``` diff --git a/specs/001-jsm-replacement/plan.md b/specs/001-jsm-replacement/plan.md deleted file mode 100644 index 20c8eb6f7e6a..000000000000 --- a/specs/001-jsm-replacement/plan.md +++ /dev/null @@ -1,220 +0,0 @@ -# Implementation Plan: Java Security Manager Replacement - -**Branch**: `15868-java-security-manager` | **Date**: 2026-04-28 | **Spec**: [spec.md](spec.md) -**JIRA**: SOLR-17767 - ---- - -## Summary - -Solr's Java Security Manager protection disappeared when JDK 24 removed the JSM API. This plan delivers a replacement using a Java agent (forked/adapted from the OpenSearch `agent-sm` module, Apache 2.0 licensed) that intercepts file access, network connections, `System.exit()`, and process spawning at the bytecode level via ByteBuddy instrumentation. The agent is activated automatically via `-javaagent:` in Solr's startup scripts and enforces a policy derived from Solr's configured directory layout. The initial release ships in **warn-only mode** to allow operator policy tuning before enabling blocking enforcement. - ---- - -## Technical Context - -**Language/Version**: Java 21 (minimum), must work on Java 24+ without deprecated APIs -**Primary Dependencies**: ByteBuddy (for bytecode instrumentation), forked from `opensearch-project/OpenSearch libs/agent-sm` (Apache 2.0) -**Storage**: No persistent storage; policy loaded from `.policy` files at JVM startup -**Testing**: JUnit 4 via Solr's randomized test framework; `SolrTestCase` / `SolrCloudTestCase` base classes -**Target Platform**: Linux, macOS, Windows (cross-platform; OS-level hardening is complementary only) -**Project Type**: Standalone Gradle subproject (`solr/agent-sm/`) + startup script integration -**Performance Goals**: ≤5% throughput degradation on standard search/indexing benchmarks -**Constraints**: Virtual-thread compatible; no `java.security.SecurityManager` API; no deprecated/removed JVM APIs -**Scale/Scope**: Applies to all code in the Solr JVM, including all loaded plugins - ---- - -## Constitution Check (AGENTS.md) - -*Re-check after Phase 1 design.* - -| Principle | Status | Notes | -|-----------|--------|-------| -| Apache License on all new source files | REQUIRED | Every new `.java` file and config file must include the ASF license header | -| All dependency versions in `gradle/libs.versions.toml` | REQUIRED | ByteBuddy version must be declared there, never in `build.gradle` directly | -| Run `gradlew updateLicenses resolveAndLockAll --write-locks` after adding ByteBuddy | REQUIRED | ByteBuddy is a new dependency; license files must be generated | -| Run `gradlew tidy` before committing Java source | REQUIRED | Code formatting enforced by Spotless | -| Run `gradlew check -x test` before declaring done | REQUIRED | Forbidden API checks, license checks, etc. | -| New test suites extend `SolrTestCase` (not `SolrTestCaseJ4`) | REQUIRED | `SolrTestCaseJ4` is deprecated for new tests | -| New classes must have javadoc | REQUIRED | Agent interceptor classes are non-obvious; javadoc is especially important | -| Wrap debug/trace log calls in `logger.isDebugEnabled()` | REQUIRED | Security interceptors are in hot paths; logging guards are essential | -| Call `coreContainer.assertPathAllowed()` for user-supplied paths | N/A | This feature replaces that pattern; new code must NOT add new call sites | -| Use `EnvUtils` to read system properties | REQUIRED | env var `SOLR_SECURITY_AGENT_MODE` → sysprop `solr.security.agent.mode` must be read via `EnvUtils` (auto-conversion is the Solr convention) | -| Use `@SuppressForbidden` for any `com.sun.*` API | LIKELY REQUIRED | ByteBuddy's bootstrap classloader injection may require `Unsafe`; needs `@SuppressForbidden` with reason | -| Changelog entry via `gradlew writeChangelog` | REQUIRED | Add after JIRA/PR is assigned | -| Reference guide update | REQUIRED | Security reference guide page needs a new section on agent-based controls | - -**No constitution violations** — all applicable principles are satisfiable within the planned approach. - ---- - -## Project Structure - -### Documentation (this feature) - -```text -specs/001-jsm-replacement/ -├── plan.md # This file -├── spec.md # Feature specification -├── research.md # Phase 0 decisions -├── data-model.md # Security policy entity model -├── quickstart.md # Operator quickstart guide -├── contracts/ -│ └── policy-file-format.md # Policy file syntax contract -└── tasks.md # Task list (52 tasks across 8 phases) -``` - -### Source Code Layout - -```text -solr/agent-sm/ -├── build.gradle # Standalone subproject build: produces agent-sm.jar with premain manifest -├── src/ -│ ├── java/org/apache/solr/security/agent/ -│ │ ├── SolrAgentEntryPoint.java # premain() + agentmain(); configures ByteBuddy -│ │ ├── SolrSecurityPolicy.java # Immutable policy singleton; loaded at startup -│ │ ├── PolicyLoader.java # Reads .policy files; variable substitution (incl. ${solr.port}, ${solr.zk.port}) -│ │ ├── FileAccessInterceptor.java # Intercepts NIO file operations; UNC + symlink handling -│ │ ├── NetworkAccessInterceptor.java # Intercepts SocketChannel.connect() / Socket.connect() -│ │ ├── ExitInterceptor.java # Intercepts System.exit() / Runtime.halt() -│ │ ├── ProcessExecInterceptor.java # Intercepts ProcessBuilder.start() / Runtime.exec() -│ │ ├── StackInspector.java # StackWalker-based call chain analysis; virtual-thread safe -│ │ ├── SecurityViolationLogger.java # Structured violation log emitter (type, target, caller, source) -│ │ └── ViolationMetricsReporter.java # LongAdder counters per type; bootstrap-safe deferred registration -│ └── test/org/apache/solr/security/agent/ -│ ├── PolicyLoaderTest.java -│ ├── FileAccessInterceptorTest.java # embedded in T026 (UNC) and T027-T028 (symlink) -│ ├── NetworkAccessInterceptorTest.java # T046 -│ ├── ExitInterceptorTest.java # T047 -│ ├── ProcessExecInterceptorTest.java # T048 -│ ├── PolicyLoaderOperatorExtensionTest.java # T025 -│ ├── UncPathRejectionTest.java # T026 -│ ├── SymlinkEscapeTest.java # T028 -│ ├── VirtualThreadCompatibilityTest.java # T030 -│ └── SolrAgentIntegrationTest.java # T024 — full-stack test with embedded Solr in enforce mode - -solr/core/src/java/org/apache/solr/core/ -└── SolrPaths.java # EXISTING: retain; add @deprecated on assertPathAllowed - -solr/server/etc/ -├── agent-security.policy # NEW: default production policy -└── agent-security-extra.policy # NEW: empty operator extension file (template) - -solr/bin/ -├── solr # MODIFY: detect agent-sm.jar, append -javaagent: -├── solr.cmd # MODIFY: same for Windows -└── solr.in.sh # MODIFY: document SOLR_SECURITY_AGENT_MODE, SOLR_SECURITY_AGENT_SKIP, SOLR_SECURITY_AGENT_EXTRA_POLICY - -solr/server/lib/ext/ -└── solr-agent-sm-*.jar # NEW: agent JAR copied here by Gradle build; detected by startup scripts - -solr/packaging/src/main/package/ -└── (MODIFY) # Add agent JAR to distribution zip/tgz - -solr/server/resources/log4j2.xml -└── (no change needed) # SecurityViolationLogger uses SLF4J → existing logging - -gradle/libs.versions.toml # ADD: byte-buddy version -``` - ---- - -## Phase 0: Research ✅ - -**Output**: [research.md](research.md) - -Key decisions made: -1. **Fork OpenSearch `agent-sm`** (Apache 2.0; same problem; same tech stack) -2. **New subproject `solr/agent-sm/`** (agent must be a standalone JAR; not a Solr module loaded inside the JVM) -3. **JDK-style `.policy` files** with Solr variable substitution -4. **Warn-only default** → enforce mode via opt-in config -5. **Auto-activation** via startup script detection of agent JAR -6. **Retain `assertPathAllowed`** as defense-in-depth; deprecate for new callers -7. **Add `ProcessExecInterceptor`** (not in OpenSearch's agent; needed for Solr) - ---- - -## Phase 1: Design & Contracts ✅ - -### Data Model - -See [data-model.md](data-model.md) for full entity definitions: -- `SecurityPolicy` — immutable root; loaded at startup -- `PermittedPath` — file system access rule (path + operations + recursive flag) -- `PermittedEndpoint` — outbound network rule (host + port range) -- `ApprovedCallSite` — allowed class for `System.exit()` or `ProcessBuilder` -- `SecurityViolation` — transient log record for blocked/warned operations - -### Contracts - -See [contracts/policy-file-format.md](contracts/policy-file-format.md): -- JDK-style `.policy` file syntax with Solr variable substitution -- Two-file approach: default policy + operator extension file -- Enforcement mode via env var `SOLR_SECURITY_AGENT_MODE` (auto-converted by `EnvUtils` to sysprop `solr.security.agent.mode`) - -### Quickstart - -See [quickstart.md](quickstart.md) — operator-facing guide covering: -- Default warn-only mode and log monitoring -- Switching to enforce mode -- Adding custom policy entries -- Diagnosing violations -- Disabling the feature (emergency only) - -### Agent Context - -See CLAUDE.md for current plan reference. - ---- - -## Implementation Phases (for /speckit-tasks) - -### Phase A: Subproject Scaffold & Policy Loading -1. Create `solr/agent-sm/build.gradle` — agent JAR with `premain`/`agentmain` manifest entries; register in `settings.gradle`; configure `copyAgentJar` task to copy output JAR to `solr/server/lib/ext/`; add agent JAR to `solr/packaging/` distribution targets -2. Add `byte-buddy` to `gradle/libs.versions.toml`; run `gradlew updateLicenses resolveAndLockAll --write-locks` -3. Implement `PolicyLoader` — reads and parses `.policy` files; Solr variable substitution -4. Implement `SolrSecurityPolicy` — immutable singleton; merges default + extra policy -5. Write unit tests for `PolicyLoader` (valid policy, malformed policy, missing file → startup failure) - -### Phase B: File Access Enforcement -1. Adapt OpenSearch `FileInterceptor` → `FileAccessInterceptor` for Solr package structure -2. Add ASF license headers; write javadoc on all public classes -3. Handle UNC path rejection and symlink resolution -4. Write unit tests covering: permitted read, denied read, UNC path, symlink escape, warn vs. enforce mode - -### Phase C: Network Access Enforcement -1. Adapt OpenSearch `SocketChannelInterceptor` → `NetworkAccessInterceptor` -2. Add `${solr.port}` and `${solr.zk.port}` variable support to `PolicyLoader`; include `*:${solr.port}` and `*:${solr.zk.port}` wildcard entries in the default policy (Decision 9 — covers all cluster nodes regardless of join order; ZK port = explicit config or `solr.port + 1000`) -3. Write unit tests covering: loopback permitted, port-wildcard entry permits any host on that port, unlisted host:port blocked - -### Phase D: Exit & Process Enforcement -1. Adapt OpenSearch `SystemExitInterceptor` + `RuntimeHaltInterceptor` → `ExitInterceptor` -2. Implement `ProcessExecInterceptor` (new — not in OpenSearch agent) for `ProcessBuilder` -3. Define default approved exit callers; define empty default exec allow-list -4. Write unit tests for each interceptor - -### Phase E: Agent Entry Point & Startup Integration -1. Implement `SolrAgentEntryPoint.premain()` — registers all ByteBuddy interceptors; loads policy -2. Modify `solr/bin/solr` and `solr/bin/solr.cmd` to detect agent JAR and add `-javaagent:` -3. Create `solr/server/etc/agent-security.policy` (default) and `agent-security-extra.policy` (empty template) -4. Write integration test: start embedded Solr with agent active; verify normal ops succeed; verify test-plugin violations are caught - -### Phase F: Documentation & Cleanup -1. Add changelog entry via `gradlew writeChangelog` -2. Update reference guide: security section with new agent-based controls -3. `@Deprecated` on `SolrPaths.assertPathAllowed` with migration note -4. Run `gradlew tidy check -x test` and fix any issues -5. Update `dev-docs/` with developer notes on adding new approved call sites - ---- - -## Risks & Mitigations - -| Risk | Likelihood | Mitigation | -|------|-----------|------------| -| ByteBuddy instrumentation breaks virtual threads | Low | OpenSearch already validated this; Solr tests will run with virtual threads enabled | -| Default policy too restrictive → test failures | Medium | All existing tests run with agent in warn mode first; violations triaged before switching to enforce | -| ByteBuddy bootstrap classloader injection rejected by JVM security flags | Low | Same technique used by OpenSearch 3.0 on Java 21/24; tested against Solr's startup flags | -| Performance regression >5% | Low | File access on hot search paths is read-only; agent interceptor is a lightweight stack walk | -| Plugin ecosystem breakage | Medium | Warn-only default gives plugin authors time to add policy extension documentation | diff --git a/specs/001-jsm-replacement/quickstart.md b/specs/001-jsm-replacement/quickstart.md deleted file mode 100644 index 4e9b786a9fd1..000000000000 --- a/specs/001-jsm-replacement/quickstart.md +++ /dev/null @@ -1,106 +0,0 @@ -# Quickstart: Java Security Manager Replacement - -**Audience**: Solr operators and plugin developers -**Feature**: Agent-based runtime security controls replacing the removed Java Security Manager - ---- - -## What This Feature Does - -Starting with this release, Solr automatically enforces boundaries around: -- **File system access** — code can only read/write directories Solr is configured to use -- **Network connections** — outbound connections are restricted to known cluster endpoints -- **JVM shutdown** — `System.exit()` can only be called from Solr's own shutdown paths -- **Process spawning** — child process creation is restricted to approved Solr components - -These protections apply to all code in the JVM, including third-party plugins, **without any code changes required**. - ---- - -## Default Behaviour - -### Enforcement mode at startup - -In this release, the default mode is **warn-only**: violations are logged but operations are not blocked. This allows operators to identify any gaps in the default policy before switching to enforce mode. - -Check for violations in `solr.log`: -``` -grep "SECURITY VIOLATION" logs/solr.log -``` - -### Switching to enforce mode - -Once satisfied that no legitimate operations are being flagged, enable enforcement: - -In `bin/solr.in.sh` (Linux/macOS): -```bash -SOLR_SECURITY_AGENT_MODE=enforce -``` - -In `bin/solr.in.cmd` (Windows): -```bat -set SOLR_SECURITY_AGENT_MODE=enforce -``` - -Then restart Solr. - ---- - -## Extending the Policy for Custom Plugins - -If your plugin reads from a non-standard directory or connects to an external service, add a policy extension file: - -**Default file**: `server/etc/agent-security-extra.policy` - -**Custom location** (read-only installs, containers, config-management): -```bash -# In bin/solr.in.sh -SOLR_SECURITY_AGENT_EXTRA_POLICY=/etc/solr/my-security-extra.policy -``` -An absent file is silently skipped — no need to create it if no extensions are required. - -**Example** — plugin reads from `/data/external` and connects to `reporting.myco.com:8080`: -``` -grant { - permission java.io.FilePermission "/data/external/-", "read"; - permission java.net.SocketPermission "reporting.myco.com:8080", "connect,resolve"; -}; -``` - -Restart Solr after editing. The extension file is merged with the default policy at startup. - ---- - -## Diagnosing Violations - -A violation log entry looks like: -``` -[WARN ] SecurityAgent - SECURITY VIOLATION [FILE_READ] target=/tmp/evil.txt caller=com.example.MyPlugin mode=WARN -``` - -**To identify the source**: The `caller` field shows the top non-JDK class in the stack. If it belongs to a known plugin, add a policy entry for that plugin's required access. - -**To see the full call stack**: Enable DEBUG logging for `org.apache.solr.security.agent`. - ---- - -## Disabling the Feature (Not Recommended) - -If you need to temporarily disable security controls during troubleshooting: - -```bash -# In bin/solr.in.sh — removes the -javaagent flag -SOLR_SECURITY_AGENT_SKIP=true -``` - -This is a temporary measure only. Running without security controls on Java 24+ provides no runtime enforcement of any kind. - ---- - -## Plugin Author Guide - -**No code changes required** for plugins that access standard Solr-managed paths and cluster endpoints. - -**If your plugin accesses external resources**, document the required policy entries in your plugin's README so operators can add them to `agent-security-extra.policy`. - -**Do not** call `SolrPaths.assertPathAllowed()` in new plugin code — the agent enforces this automatically. The method is retained for internal Solr use only. diff --git a/specs/001-jsm-replacement/research.md b/specs/001-jsm-replacement/research.md deleted file mode 100644 index 99b91521dc04..000000000000 --- a/specs/001-jsm-replacement/research.md +++ /dev/null @@ -1,241 +0,0 @@ -# Phase 0 Research: Java Security Manager Replacement - -**Date**: 2026-04-28 -**Feature**: specs/001-jsm-replacement/spec.md - ---- - -## Decision 1: Adopt, Fork, or Build the Java Agent - -**Decision**: Fork/adapt the OpenSearch `agent-sm` module rather than building from scratch. - -**Rationale**: -- The OpenSearch agent is Apache 2.0 licensed — fully compatible with Apache Solr's licensing. -- It directly solves the same problem (JSM removal in JDK 24), with the same four protection categories required by this spec: file access, network access, System.exit(), process execution. -- It uses ByteBuddy (already widely used in the Java ecosystem) and StackWalker (standard since Java 9), both compatible with Java 21+. -- It is virtual-thread compatible (no thread-identity assumptions). -- Building equivalent infrastructure from scratch would take significantly longer with no functional advantage. -- Solr can strip out OpenSearch-specific policy entries (e.g., BouncyCastle, script permissions) and adapt the policy defaults to Solr's directory layout. - -**Alternatives considered**: -- **Build from scratch**: Higher effort, same outcome, no benefit. -- **Use a third-party security library (e.g., Byte Buddy agent SPI)**: No existing library covers the full set of interceptions needed (file + network + exit + exec). Would still require significant custom work. -- **Rely solely on OS-level hardening (systemd)**: Fails the cross-platform constraint (not available on Windows/macOS). - ---- - -## Decision 2: Subproject Location - -**Decision**: Create a new Gradle subproject `solr/agent-sm/` (directly under `solr/`, not under `solr/modules/`). - -**Rationale**: -- The agent JAR must be on the JVM command line as `-javaagent:`, loaded by the JVM *before* any application classloader. It is fundamentally different from a Solr module (which is loaded inside the running Solr JVM by Solr's own classloading machinery). -- Placing it under `solr/modules/` would be misleading — Solr modules are components that Solr itself loads and manages at runtime. The agent is an external JVM concern that Solr does not load; the JVM does. -- `solr/agent-sm/` is structurally parallel to other non-module subprojects like `solr/core/`, `solr/solrj/`, and `solr/test-framework/`, which is the correct category for a build artifact that is JVM-level infrastructure. -- Mirroring the OpenSearch layout (`libs/agent-sm/`) is closest in spirit, but Solr does not use a top-level `libs/` directory; `solr/` is the equivalent container. - -**Alternatives considered**: -- **`solr/modules/agent-sm/`**: Misleading — modules are Solr-level, not JVM-level. Would also imply the agent is optional/pluggable in the same sense as, e.g., `jwt-auth`, which it is not. -- **`solr/core/` integration**: Not possible — agent must be a standalone fat JAR with `Premain-Class` manifest entry, produced independently of `solr-core.jar`. -- **Top-level `agent-sm/`**: Would sit outside the `solr/` tree, inconsistent with where all Solr Java source lives. - ---- - -## Decision 3: Policy Configuration Format - -**Decision**: Use JDK-style `.policy` file syntax (same format as the existing `solr/server/etc/security.policy`), extended with Solr-specific variable substitution (e.g., `${solr.home}`, `${solr.data.dir}`). - -**Rationale**: -- Solr already ships a `security.policy` file; operators are familiar with the syntax. -- OpenSearch's `agent-sm` already implements a `PolicyFile` parser that reads JDK-style `.policy` files — this can be adapted directly. -- Solr-specific variable substitution (`${solr.home}` etc.) avoids hardcoding paths and makes the default policy portable across installations. - -**Alternatives considered**: -- **Custom YAML/JSON config**: More readable but requires a new parser, new documentation, and breaks familiarity with JSM policy syntax. -- **Inline configuration in `solr.xml`**: Too verbose; policy files can be large; separating concerns is cleaner. - ---- - -## Decision 4: Enforcement Mode — Enforce vs. Warn-Only - -**Decision**: Ship with a **warn-only mode** as the initial default, with an **enforce mode** opt-in via configuration. A future release will flip the default to enforce. - -**Rationale**: -- Solr has a large plugin ecosystem; some plugins may legitimately access paths or hosts not covered by the default policy. -- A hard-blocking default would cause regressions for valid third-party plugins, violating FR-013. -- Warn-only allows operators to identify gaps in their custom policy before flipping to enforce. -- This matches how the old JSM was typically introduced: test first, enforce later. - -**Alternatives considered**: -- **Enforce by default immediately**: Higher risk of regressions for plugin authors; may delay adoption. -- **Warn-only permanently**: Defeats the security purpose; not acceptable long term. - ---- - -## Decision 9: Default Network Policy for Intra-Cluster Connectivity - -**Decision**: The default permitted network endpoint list uses **port-based wildcards** rather than a host-derived list: permit any host on `` (inter-node HTTP) and any host on the ZooKeeper port (explicit config, or ` + 1000` for embedded ZK). - -**Rationale**: -- SolrCloud nodes join the cluster incrementally after ZooKeeper registers them. A node list derived at agent startup will always be incomplete, causing legitimate inter-node connections to be blocked in enforce mode for nodes that join later. -- Port-based wildcards allow all current and future cluster nodes to communicate without any operator action or restart. -- The port restriction still prevents connections to arbitrary external services — an attacker or rogue plugin cannot use the intra-cluster policy to reach, e.g., a database on an unexpected port. -- Inter-node Solr HTTP is already authenticated via PKI, so the broadened host permission does not weaken authentication. -- This removes the previously documented "known limitation" that topology changes require a restart. - -**Alternatives considered**: -- **Dynamic ZK watcher**: Subscribe to ZK node join/leave events and update permitted endpoints at runtime. Tightest security, but adds significant complexity to the agent and creates a dependency on ZK client libraries in the agent JAR. -- **Auto-detect local subnet**: Derive `/24` from the primary network interface. Avoids needing to know the port but may be ambiguous on multi-homed hosts or containers. -- **Operator-configured subnet** (`SOLR_SECURITY_AGENT_CLUSTER_SUBNET`): Explicit but adds operator friction; incorrect configuration silently breaks the cluster. -- **Startup-only host derivation**: Rejected because in practice the ZK-known node list is always incomplete at startup. - -**Default permitted entries added by this decision**: -- `*:` — connect,resolve (inter-node Solr HTTP; default port 8983) -- `*:` — connect,resolve (ZK port from config, or `solr.port + 1000` for embedded ZK) -- External ZK ensemble hosts:ports from the ZK connection string (specific hosts, added at startup) - ---- - -## Decision 5: Activation Mechanism - -**Decision**: The agent JAR is added to Solr's startup scripts (`solr.in.sh` / `solr.in.cmd`) as a `-javaagent:` JVM argument, automatically, when the module is present. - -**Rationale**: -- Java agents must be loaded via `-javaagent:` before the application starts — they cannot be loaded on demand at runtime. -- Solr's startup scripts already have a pattern for conditionally appending JVM args (e.g., for GC logging, heap dumps). -- The module JAR being present in `server/modules/agent-sm/` is sufficient signal; the startup script detects it and adds the flag. - -**Alternatives considered**: -- **Always-on (bundled in core)**: Not technically feasible for a Java agent. -- **Manual operator configuration of JVM args**: Too error-prone; operators would forget; protections would silently not apply. - ---- - -## Decision 6: Disposition of SolrPaths.assertPathAllowed - -**Decision**: Retain all existing `SolrPaths.assertPathAllowed` call sites as defense-in-depth, but document them as no longer the primary enforcement mechanism. - -**Rationale**: -- Removing 35 call sites is unnecessary churn and removes defense-in-depth for code paths that may not be instrumented. -- The agent-based enforcement is the primary layer; `assertPathAllowed` becomes a secondary redundant check. -- New code MUST NOT add new `assertPathAllowed` call sites — the agent handles it automatically. - ---- - -## Decision 7: Process Execution Control Implementation - -**Decision**: Instrument `ProcessBuilder` and `Runtime.exec()` at the bytecode level (via agent) to restrict spawning to an allow-listed set of call-site class name prefixes. The default allow-list covers Solr's known legitimate process-spawning call sites. - -**Rationale**: -- Existing `ProcessBuilder` usage in Solr is minimal (3 sites: SolrProcessManager, IpTables test helper). -- OpenSearch's `agent-sm` does not include a ProcessBuilder interceptor — Solr will need to add one. -- Class-prefix allow-listing is simple, auditable, and requires no runtime performance cost for non-spawning code paths. - -**Known approved call sites**: -- `org.apache.solr.cli.SolrProcessManager` (JVM discovery via ProcessHandle — does not use ProcessBuilder directly; kept as approved for future compatibility) -- `org.apache.solr.cloud.IpTables` (test-only; listed in test policy, not production policy) - ---- - -## Decision 8: Metrics Registration Bridge (Classloader Boundary) - -**Decision**: Use reflection-based late binding from `CoreContainer` to `ViolationMetricsReporter`. No compile-time dependency between `solr:core` and `solr:agent-sm` is introduced. - -**Rationale**: -- The Java agent runs in the bootstrap classloader (injected via `ClassInjector.UsingUnsafe.ofBootLoader()`). Classes loaded there are visible to all classloaders, including the application classloader used by `solr:core`. -- A compile-time dependency `solr:core → solr:agent-sm` would be circular if agent-sm ever needs core types, and would also put agent classes on the application classpath rather than the bootstrap classpath — defeating the instrumentation model. -- Reflection avoids the compile-time coupling while still allowing CoreContainer to call `ViolationMetricsReporter.registerWithSolrMetrics()` at runtime if and only if the agent is loaded. -- `ClassNotFoundException` is caught silently: if the agent JAR is not present (e.g., `SOLR_SECURITY_AGENT_SKIP=true`), metrics registration is simply skipped. No NPE, no startup failure. - -**Implementation pattern** (in `CoreContainer`): -```java -try { - Class reporter = Class.forName( - "org.apache.solr.security.agent.ViolationMetricsReporter", - false, - null // null = bootstrap classloader - ); - reporter.getMethod("registerWithSolrMetrics", SolrMetricManager.class, String.class) - .invoke(null, metricManager, "solr.jvm"); -} catch (ClassNotFoundException ignored) { - // Agent not loaded; metrics registration skipped -} catch (ReflectiveOperationException e) { - log.warn("Failed to register security agent metrics", e); -} -``` - -**Alternatives considered**: -- **Shared interface module `solr:agent-sm-api`**: Both `solr:core` and `solr:agent-sm` would depend on it. Cleaner but adds a new module with a single interface. Overkill for one method; deferred to future if the API grows. -- **ServiceLoader SPI**: Requires the agent JAR on the application classpath, not bootstrap. Architecturally incorrect for a Java agent. -- **Direct compile-time dependency**: Circular; breaks bootstrap classloader model. Rejected. - ---- - -## Codebase Findings Summary - -| Area | Finding | -|------|---------| -| Java minimum | 21 (`gradle/libs.versions.toml`: `java-min = "21"`) | -| Existing policy files | `solr/server/etc/security.policy`, `gradle/testing/randomization/policies/solr-tests.policy` | -| SolrPaths.assertPathAllowed | `solr/core/src/java/org/apache/solr/core/SolrPaths.java`; 35 call sites | -| ProcessBuilder usage | 3 sites (SolrProcessManager, IpTables test helper, ProcessManager test) | -| Subproject pattern reference | `solr/core/`, `solr/solrj/` (agent is JVM-level, not a Solr module) | -| Test base classes | `SolrTestCase`, `SolrCloudTestCase` | -| No SecurityManager active | Policy files exist but no `System.setSecurityManager()` calls | -| No existing security module | Closest is `solr/modules/jwt-auth/` | - ---- - -## Codebase Finding: Module Outbound Network Connections - -Analysis of server-side outbound network calls (excluding ZooKeeper and inter-Solr-node HTTP, which are covered by the default policy). This informs T044 (module ref guide NOTICE boxes) and T045 (commented examples in `agent-security-extra.policy`). - -| Module | External Service | Protocol | Component | Notes | -|--------|----------------|----------|-----------|-------| -| `jwt-auth` | OIDC Identity Provider (well-known + JWKS) | HTTPS | `JWTIssuerConfig`, `HttpsJwksFactory` | Uses `jose4j`; host is operator-configured; may refresh JWKS at runtime | -| `extraction` | Apache Tika Server | HTTP/HTTPS | `TikaServerExtractionBackend` | Uses Jetty `HttpClient`; only in remote-Tika mode — embedded Tika makes no outbound calls | -| `opentelemetry` | OTLP collector (Jaeger, Grafana Tempo, etc.) | gRPC or HTTP | `OtlpExporterFactory` | Host from `OTEL_EXPORTER_OTLP_ENDPOINT`; continuous background export | -| `s3-repository` | Amazon S3 API (or S3-compatible) | HTTPS | `S3StorageClient` | AWS SDK v2; uses `*.amazonaws.com` or operator-configured custom endpoint | -| `gcs-repository` | Google Cloud Storage API | HTTPS | `GCSBackupRepository` | GCS SDK; uses `storage.googleapis.com` | -| `cross-dc-manager` | Apache Kafka brokers | TCP (Kafka protocol) | `KafkaCrossDcConsumer`, `KafkaRequestMirroringHandler` | Broker addresses fully operator-configured | -| `core` (ZK status admin) | ZooKeeper four-letter-word socket | TCP (raw socket, port 2181) | `ZookeeperStatusHandler` | Same ZK hosts as cluster; already covered by the default ZK policy entry | - -**No outbound network calls found in**: `langid` (local NLP only), `ltr` (local feature processing), `BasicAuthPlugin` (in-memory), `analytics`. - -### Handling approach — superseded by Decision 10 - -See Decision 10 below. The original plan (NOTICE boxes for all 6 modules) was refined after per-module trust assessment to pre-permit 5 of the 6 modules directly in the bundled agent policy. - ---- - -## Decision 10: Per-Module Wildcard Network Pre-Permitting - -**Decision**: For five of the six outbound-connecting bundled modules, add a `SocketPermission "*", "connect,resolve"` grant scoped to that module's codebase in the bundled agent policy (`agent-security.policy`). The `extraction` module is excluded. - -**Pre-permitted modules and rationale**: - -| Module | External Service | Who Configures Endpoint | Trust Assessment | -|--------|----------------|------------------------|-----------------| -| `jwt-auth` | OIDC IdP + JWKS | `security.json`, requires `SECURITY_EDIT` (Solr admin) | **Safe** — admin-only | -| `opentelemetry` | OTLP collector | Env var / system property / solr.xml — node admin only | **Safe** — node admin only | -| `s3-repository` | Amazon S3 / S3-compat | solr.xml backup handler config — node admin | **Safe** — node admin only | -| `gcs-repository` | Google Cloud Storage | solr.xml backup handler config — node admin | **Safe** — node admin only | -| `cross-dc-manager` | Apache Kafka | Env var / system property / ZooKeeper — node + cluster admin | **Safe** — admin-only | -| `extraction` | Remote Tika Server | `solrconfig.xml` `requestHandler` — `CONFIG_EDIT` privilege (collection admin) | **SSRF risk** — excluded | - -**Why `extraction` is excluded**: The Tika Server URL is set in `solrconfig.xml` via `TikaConfig`, which is modifiable by collection administrators with `CONFIG_EDIT` privilege — a broader group than node administrators. A wildcard `SocketPermission` would allow a collection admin to point the Tika client at any internal host, enabling SSRF attacks against the cluster's internal network from within the Solr JVM. Operators who use remote Tika must add an explicit host-locked policy entry to `agent-security-extra.policy`. - -**Implementation**: Each pre-permitted module's bundled policy grant uses a `codeBase` restriction to limit the wildcard to that module's JAR: -``` -grant codeBase "file:${solr.install.dir}/modules/jwt-auth/-" { - permission java.net.SocketPermission "*", "connect,resolve"; -}; -``` -This prevents other code from benefiting from the module's wildcard grant. - -**Effect on T044/T045**: Scope is reduced to the `extraction` module only. No NOTICE boxes or `agent-security-extra.policy` examples are needed for the five pre-permitted modules. - -**Alternatives considered**: -- **NOTICE boxes for all 6 modules**: Higher operator friction; legitimate use cases (e.g., OTel metrics export) require manual action on every new cluster. Rejected. -- **Wildcard grant for all 6 including extraction**: Creates SSRF risk for the extraction module. Rejected. -- **Per-hostname grants auto-derived from config at startup**: Config is not available to the agent at JVM launch time; would require complex late-binding. Rejected. diff --git a/specs/001-jsm-replacement/spec.md b/specs/001-jsm-replacement/spec.md deleted file mode 100644 index 00e90023378f..000000000000 --- a/specs/001-jsm-replacement/spec.md +++ /dev/null @@ -1,190 +0,0 @@ -# Feature Specification: Java Security Manager Replacement - -**Feature Branch**: `15868-java-security-manager` -**Created**: 2026-04-28 -**Status**: Draft -**JIRA**: SOLR-17767 - -## Background - -Java Security Manager (JSM) was removed in JDK 24. Solr historically relied on it to enforce boundaries around file access, network traffic, process execution, and JVM lifecycle operations. Solr currently has a partial mitigation (`SolrPaths.assertPathAllowed`) for file paths, but it requires developers to call it explicitly — nothing enforces that they do. There is no equivalent mitigation for network access, Windows UNC path attacks, `System.exit()` abuse, or arbitrary process spawning. - -This feature introduces automatic, cross-platform runtime security controls covering the highest-risk categories: file system access, outbound network connections, JVM shutdown prevention, and process execution restriction. - ---- - -## User Scenarios & Testing - -### User Story 1 — Standard Solr Deployment Is Protected by Default (Priority: P1) - -A Solr operator upgrades to a Java 24+ JVM (or any supported JVM). Without any configuration change, the new security controls automatically enforce boundaries around file access, network calls, `System.exit()`, and process spawning. Plugins loaded by Solr are also subject to these controls. - -**Why this priority**: This is the core goal of the feature. All existing Solr deployments should gain protection automatically — no manual action required. - -**Independent Test**: Deploy a standard Solr instance. Load a test plugin that attempts to read a file outside Solr's permitted directories. Verify the read is blocked and a warning is logged. Verify all standard Solr operations (search, indexing, admin) continue to function normally. - -**Acceptance Scenarios**: - -1. **Given** a Solr server running with default configuration, **When** any code (including a plugin) attempts to read a file outside the permitted directory tree, **Then** the operation is denied and a log entry identifies the blocked path and the call origin. -2. **Given** a Solr server running with default configuration, **When** any code attempts to open an outbound network connection to a host not in the permitted list, **Then** the connection is refused and logged. -3. **Given** a Solr server running with default configuration, **When** plugin code calls `System.exit()`, **Then** the JVM does not terminate and the violation is logged. -4. **Given** a Solr server running with default configuration, **When** code attempts to spawn a child process from an unauthorized call site, **Then** the process is not created and the violation is logged. -5. **Given** a Solr server running with default configuration, **When** a standard search or indexing request is processed, **Then** all operations complete successfully with no security violations triggered. - ---- - -### User Story 2 — Operator Extends Policy for Custom Deployment (Priority: P2) - -A Solr operator runs a non-standard deployment: for example, a plugin that legitimately reads from an external NFS mount, or a connector that makes outbound HTTP calls to a specific SaaS endpoint. The operator needs to authorize these operations without disabling security controls globally. - -**Why this priority**: Real-world deployments have legitimate needs beyond the default policy. Without this, operators would be forced to disable the feature entirely. - -**Independent Test**: Configure a custom policy entry that permits a specific additional file path. Verify that access to that path succeeds. Verify that access to a different unauthorized path is still blocked. - -**Acceptance Scenarios**: - -1. **Given** an operator has added a permitted file path to the policy configuration, **When** Solr code reads a file at that path, **Then** the read succeeds. -2. **Given** an operator has permitted a specific outbound host, **When** a plugin connects to that host, **Then** the connection succeeds. -3. **Given** an operator has not permitted a path or host, **When** code attempts to access it, **Then** the access is denied regardless of any custom entries. - ---- - -### User Story 3 — Windows UNC Paths Are Blocked (Priority: P2) - -An attacker or misconfigured plugin constructs a Windows UNC path (`\\attacker-host\share\...`) to exfiltrate data or probe internal networks. On a Windows host, the OS would initiate an outbound SMB connection; on any host, UNC paths bypass standard path validation. - -**Why this priority**: UNC paths are a well-known bypass vector for path-based access controls. The current `SolrPaths.assertPathAllowed` does not protect against them. - -**Independent Test**: Attempt a file read using a UNC-style path. Verify the operation is denied and logged on all platforms. - -**Acceptance Scenarios**: - -1. **Given** any Solr deployment on any supported platform, **When** code attempts to access a path beginning with `\\`, **Then** the access is denied and logged. -2. **Given** a legitimate file read using a standard absolute path, **When** the path resolves within permitted directories, **Then** the read succeeds normally. - ---- - -### User Story 4 — Symlink Escape Is Prevented (Priority: P2) - -An attacker or plugin creates a symlink inside a permitted directory that points to a sensitive path outside it (e.g., `/etc/passwd`). Under the old JSM, following symlinks into unauthorized directories was blocked. This protection must continue. - -**Why this priority**: Symlink attacks are a classic privilege escalation path that specifically targets path-based access controls. - -**Independent Test**: Create a symlink inside the Solr data directory pointing to `/etc/passwd`. Attempt to read via the symlink. Verify the read is denied. - -**Acceptance Scenarios**: - -1. **Given** a symlink inside a permitted directory pointing to a path outside permitted directories, **When** code follows the symlink and reads the target, **Then** the read is denied and logged. - ---- - -### User Story 5 — Security Controls Are Compatible with Virtual Threads (Priority: P3) - -A Solr deployment uses Project Loom virtual threads. The security controls must not interfere with virtual thread scheduling or produce incorrect enforcement decisions based on thread identity. - -**Why this priority**: Virtual thread compatibility is a hard constraint — Solr is moving toward Loom and JSM itself was incompatible with it. - -**Independent Test**: Enable virtual threads; run a standard workload. Verify no false positives (legitimate operations blocked) or false negatives (violations not caught). - -**Acceptance Scenarios**: - -1. **Given** Solr is running with virtual threads enabled, **When** standard operations execute, **Then** no security violations are incorrectly triggered. -2. **Given** a virtual thread executes unauthorized file access, **When** the access is attempted, **Then** it is blocked just as it would be on a platform thread. - ---- - -### Edge Cases - -- What happens when a security violation occurs in a critical path during startup? The violation must be logged and startup must fail fast with a clear error rather than silently continuing. -- How does the system behave when the policy configuration file is absent or malformed? Solr must refuse to start and log a clear error rather than running without any security controls. -- What happens when a permitted path is deleted at runtime? Access to paths that were permitted but no longer exist must still be governed by the policy (i.e., a missing permitted path does not grant access to its parent). -- What happens on platforms where process spawning is used internally by Solr (e.g., the `bin/solr` script launcher)? Approved internal call sites must be explicitly listed in the default policy. - ---- - -## Requirements - -### Functional Requirements - -- **FR-001**: The system MUST automatically apply security controls to all code running in the Solr JVM, including third-party plugins, without requiring developers to add per-call-site checks. -- **FR-002**: The system MUST deny file reads and writes to paths outside the set of permitted directories derived from Solr's configured home, data, log, and temporary directories. -- **FR-003**: The system MUST deny file access to Windows UNC paths (`\\host\share\...`) on all platforms by default. -- **FR-004**: The system MUST deny file access when a symlink resolves to a target outside the permitted directories. -- **FR-005**: The system MUST deny outbound network connections to hosts and ports not declared in the permitted endpoint list. The default list MUST include: loopback addresses, any host on the configured Solr HTTP port (`*:`), any host on the ZooKeeper port (explicit config or ` + 1000` for embedded ZK), and a wildcard `SocketPermission "*", "connect,resolve"` scoped to the codebases of the five pre-permitted bundled modules (`jwt-auth`, `opentelemetry`, `s3-repository`, `gcs-repository`, `cross-dc-manager`), so that all current and future cluster nodes and these trusted modules are reachable without operator intervention. -- **FR-006**: The system MUST prevent `System.exit()` from terminating the JVM except when called from a small, explicitly approved set of Solr shutdown call sites. -- **FR-007**: The system MUST prevent spawning of child processes except from an explicitly approved, auditable set of call sites in Solr core. -- **FR-008**: Every security violation MUST produce a log entry at WARN level or higher that identifies: the blocked operation type, the target (path, host, or operation), and sufficient call-site context to locate the offending code. -- **FR-009**: Operators MUST be able to extend the default security policy by adding permitted file paths, network endpoints, or approved call sites via the `agent-security-extra.policy` file, without modifying Solr source code. The path to this file MUST default to `${server.dir}/etc/agent-security-extra.policy` and MUST be overridable via the `SOLR_SECURITY_AGENT_EXTRA_POLICY` environment variable (auto-converted to system property `solr.security.agent.extra.policy`). An absent extra policy file MUST be silently skipped; it is not a startup error. -- **FR-010**: The security controls MUST function correctly on Java 21 and Java 24+, without relying on any deprecated or removed JVM API. -- **FR-011**: The security controls MUST be compatible with Project Loom virtual threads; enforcement decisions MUST NOT depend on thread identity. -- **FR-012**: The security controls MUST NOT cause a measurable throughput degradation of more than 5% on standard Solr search and indexing workloads compared to an equivalent deployment without controls. -- **FR-013**: All Solr functionality that works in a standard deployment today MUST continue to work without modification when security controls are enabled. -- **FR-014**: The security controls MUST be cross-platform; they MUST apply on Linux, macOS, and Windows without relying on OS-specific mechanisms as the sole enforcement layer. -- **FR-015**: The system MUST refuse to start if the security policy configuration is absent or invalid, and MUST log a clear diagnostic message. -- **FR-016**: The agent security controls and their configuration (policy file format, enforcement mode, operator extension mechanism, and startup options) MUST be documented in the Solr reference guide as a dedicated section visible to end users and operators. Additionally, all new environment variables and system properties introduced by this feature (`SOLR_SECURITY_AGENT_MODE` / `solr.security.agent.mode`, `SOLR_SECURITY_AGENT_SKIP`, `SOLR_SECURITY_AGENT_EXTRA_POLICY` / `solr.security.agent.extra.policy`) MUST be added to `solr-properties.adoc` with their default values and descriptions, consistent with the format of existing entries in that file. -- **FR-017**: The system MUST expose security violation counts, broken down by type (file access, network, exit, process exec), in Solr's metrics registry so operators can monitor and alert on violations without log parsing. - -### Key Entities - -- **Security Policy**: The complete set of rules defining what file paths, network endpoints, and process operations are permitted. Has a default configuration and supports operator-defined extensions. -- **Permitted Path**: A directory (or file pattern) that code running in the Solr JVM is allowed to read from or write to. Derived from Solr's configured layout by default. -- **Permitted Endpoint**: A host-and-port pair (or pattern) that code is allowed to connect to outbound. The default list covers intra-cluster traffic via port-based wildcards (`*:`, `*:`); all other external traffic requires explicit operator policy entries. -- **Approved Call Site**: A specific class (or class pattern) in Solr core that is permitted to call `System.exit()` or spawn a child process. -- **Security Violation**: A blocked operation. Carries operation type, target, timestamp, and call-site context. Emitted as a log entry. - ---- - -## Success Criteria - -### Measurable Outcomes - -- **SC-001**: 100% of standard Solr integration tests pass without modification when security controls are enabled, on both Java 21 and Java 24+. -- **SC-002**: Throughput on standard search and indexing benchmarks degrades by no more than 5% with controls enabled versus disabled. -- **SC-003**: Every attempted unauthorized file access, outbound connection, `System.exit()` call, and unauthorized process spawn produces a log entry within the same operation, on 100% of attempts in automated tests. -- **SC-004**: A plugin attempting to read a file outside the permitted directory tree is blocked on 100% of attempts, including when the path uses symlinks or Windows UNC notation. -- **SC-005**: An operator can configure a custom permitted file path or network endpoint and have it take effect after a Solr restart, following only the published documentation. -- **SC-006**: Solr starts and operates normally under virtual threads with security controls enabled, with no false-positive security violations in the standard test suite. -- **SC-007**: After at least one security violation occurs, the corresponding per-type counter in the metrics registry is non-zero and readable via the standard metrics API, confirming metric registration succeeded. -- **SC-008**: The `solr/agent-sm/` module test suite runs with the agent in enforce mode and passes with zero unexpected violations; the broader Solr test suite runs in warn mode with zero unexpected violations logged. - ---- - -## Clarifications - -### Session 2026-04-28 - -- Q: Must the agent and its configuration be documented in the Solr reference guide? → A: Yes — FR-016 added requiring a dedicated reference guide section covering policy file format, enforcement mode, operator extension mechanism, and startup options. -- Q: Should security violations be exposed as Solr metrics in addition to log entries? → A: Yes, broken down by type (FILE, NETWORK, EXIT, EXEC) in the metrics registry — FR-017 added. -- Q: Should the agent run in enforce mode during the Solr test suite? → A: Enforce mode within the `solr/agent-sm/` module tests only; warn mode for the broader Solr test suite initially, with a planned follow-up to flip to enforce once the violation log is clean. -- Q: Should the network policy refresh when SolrCloud cluster topology changes at runtime? → A: No — startup-only; topology changes require a restart. Documented as a known limitation; workaround is permitting cluster subnet via the extension policy file. *(Superseded by Q7 below.)* -- Q: How should the default network policy handle SolrCloud inter-node connectivity, given that the full node set is not known at agent startup? → A: Permit any host on the configured Solr HTTP port (`*:`) and on the ZooKeeper port (explicit config or `solr.port + 1000` for embedded ZK). No host restriction for intra-cluster traffic; port restriction prevents connections to arbitrary external services on other ports. The startup-only topology derivation assumption is removed; no restart is required when new nodes join the cluster. -- Q: What happens to the existing JSM-era policy files (`security.policy`, `solr-tests.policy`)? → A: Retain both with deprecation notices in this release; schedule removal in a future release. -- Q: When does warn mode become enforce-by-default, and should warn mode log a startup advisory? → A: No committed timeline; the flip is a future decision. Warn mode does not log an advisory message. - -### Session 2026-04-29 - -- Q: Should bundled modules that make outbound network connections have their endpoint host pre-permitted in the default agent policy, or should each require an explicit operator `agent-security-extra.policy` entry? → A: Pre-permit with a `SocketPermission "*", "connect,resolve"` wildcard in the **bundled agent policy** for the five modules whose external endpoint is exclusively controlled by a node/cluster admin (`jwt-auth`, `opentelemetry`, `s3-repository`, `gcs-repository`, `cross-dc-manager`). The `extraction` module is excluded from this pre-permitting because its Tika Server URL is configurable via `solrconfig.xml` at `CONFIG_EDIT` privilege level, creating an SSRF risk with a wildcard grant; operators must add an explicit policy entry for it. T044 and T045 scope is reduced to the `extraction` module only. - -### Session 2026-04-30 - -- Q: Must the new env vars/sysprops introduced by this feature (`SOLR_SECURITY_AGENT_MODE`, `SOLR_SECURITY_AGENT_SKIP`, `SOLR_SECURITY_AGENT_EXTRA_POLICY` and their sysprop equivalents) be documented in the ref-guide `solr-properties.adoc` properties reference page? → A: Yes — FR-016 extended to explicitly require `solr-properties.adoc` entries for all three new variables, in addition to the dedicated security section. -- Q: Should the path to `agent-security-extra.policy` be fixed or configurable (to support read-only installs, container images, and config-management tooling)? → A: Fixed default (`${server.dir}/etc/agent-security-extra.policy`) with an env/sysprop override: `SOLR_SECURITY_AGENT_EXTRA_POLICY` env var, auto-converted to system property `solr.security.agent.extra.policy`. An absent file is silently skipped (not a startup error). `solr.xml` is not a supported source for this setting (the agent loads before `solr.xml` is parsed). - ---- - -## Assumptions - -- Solr's configured home, data, log, and temporary directories are the authoritative source for the default permitted file path set; no additional paths are permitted by default. -- The default permitted network endpoint list covers intra-cluster communication via port-based wildcards: any host on `` (inter-node HTTP) and any host on the ZooKeeper port (explicit config or ` + 1000` for embedded ZK). This ensures all cluster nodes — including those that join after startup — are reachable without operator intervention or a restart. -- Loopback addresses (`localhost`, `127.0.0.1`, `::1`) are unconditionally permitted in the default policy. -- External ZooKeeper ensemble hosts on non-standard ports (i.e., a ZK ensemble not co-located with Solr on the standard port offset) are added to the permitted list from the ZK connection string configuration at startup. -- OS-level hardening (e.g., `systemd` unit file restrictions on Linux) is treated as a complementary layer and is documented but not required for the in-JVM controls to function. -- Removing `SolrPaths.assertPathAllowed` call sites is out of scope for this feature; the automatic controls supersede them, but legacy call sites are retained for defense-in-depth. -- Fine-grained access controls (reflection, class loading, runtime permissions beyond the four categories above) are explicitly out of scope. -- The OpenSearch `agent-sm` module (Apache 2.0 licensed) is a candidate for adoption or forking and will be evaluated during planning; this specification is technology-agnostic with respect to that choice. -- Security controls are enabled by default in warn mode in new installations and when upgrading, with a documented opt-out mechanism (`SOLR_SECURITY_AGENT_SKIP=true`) for operators who need to disable them temporarily. The timeline for making enforce mode the default is not committed; it will be decided in a future release based on test suite and community readiness. Warn mode does not emit a startup advisory message. -- The existing `solr/server/etc/security.policy` and `gradle/testing/randomization/policies/solr-tests.policy` files are retained in this release with deprecation notices (added as comments); they are no longer enforced by the JVM and serve as migration references only. Removal is planned for a future release. -- The `solr-tests.policy` file continues to be used by the randomized test framework for purposes unrelated to the agent; it must not be deleted in this release. - -- The `agent-security-extra.policy` file path defaults to `${server.dir}/etc/agent-security-extra.policy`. It can be overridden via `SOLR_SECURITY_AGENT_EXTRA_POLICY` env var / `solr.security.agent.extra.policy` sysprop to support read-only install trees, container images, and config-management tooling. The file's absence is not an error. The `solr.xml` element is not supported for this setting because the agent initialises before `solr.xml` is parsed. -- Several optional modules make outbound connections to operator-configured external services. For five of them (`jwt-auth`, `opentelemetry`, `s3-repository`, `gcs-repository`, `cross-dc-manager`), the external endpoint address is exclusively configurable by a node or cluster administrator, so a `SocketPermission "*", "connect,resolve"` wildcard is pre-permitted in the bundled agent policy — operators need no manual policy entry to use these modules. The `extraction` module is excluded from pre-permitting because its Tika Server URL is configurable via `solrconfig.xml` at `CONFIG_EDIT` privilege (collection-admin level), which would allow a collection administrator to redirect the permission to an arbitrary SSRF target; operators who use remote Tika must add an explicit policy entry. The `extraction` module's reference guide page carries a policy NOTICE box with a ready-to-paste snippet (T044/T045). See [research.md — Module Outbound Network Connections](research.md) for the full per-module analysis. diff --git a/specs/001-jsm-replacement/tasks.md b/specs/001-jsm-replacement/tasks.md deleted file mode 100644 index 68d377ef49ac..000000000000 --- a/specs/001-jsm-replacement/tasks.md +++ /dev/null @@ -1,271 +0,0 @@ -# Tasks: Java Security Manager Replacement - -**Input**: Design documents from `specs/001-jsm-replacement/` -**Branch**: `15868-java-security-manager` | **JIRA**: SOLR-17767 -**Plan**: plan.md | **Spec**: spec.md (incl. clarifications 2026-04-28, 2026-04-29, 2026-04-30) - -**New since previous version**: FR-016 (reference guide docs), FR-017 (violation metrics), SC-007/SC-008 (metrics + enforce-mode tests), old policy file deprecation, enforce mode scoped to agent-sm tests only, Decision 9 (port-wildcard intra-cluster network policy), T044/T045 (module outbound network — extraction NOTICE box only; 5 modules pre-permitted with wildcard in bundled policy per clarification 2026-04-29), T050/T047b (configurable extra-policy path via `SOLR_SECURITY_AGENT_EXTRA_POLICY` per clarification 2026-04-30), T048b (new agent env vars/sysprops documented in `solr-properties.adoc` per clarification 2026-04-30). - -**Organization**: Tasks grouped by user story (US1–US5) to enable independent implementation and testing. - -## Format: `[ID] [P?] [Story] Description` - -- **[P]**: Can run in parallel (different files, no incomplete-task dependencies) -- **[Story]**: Which user story this implements ([US1]–[US5]) - ---- - -## Phase 1: Setup (Gradle Subproject Scaffold) - -**Purpose**: Create the `solr/agent-sm/` Gradle subproject and wire it into the build. All subsequent work depends on this. - -- [X] T001 Register `solr/agent-sm` as a Gradle subproject in `settings.gradle` (add `include 'solr:agent-sm'`) -- [X] T002 Create `solr/agent-sm/build.gradle` — apply `java-library`; configure agent JAR manifest with `Premain-Class: org.apache.solr.security.agent.SolrAgentEntryPoint` and `Can-Redefine-Classes: true`; declare ByteBuddy as `implementation` dependency -- [X] T003 Add `byte-buddy` version entry to `gradle/libs.versions.toml` (match version used by OpenSearch agent-sm; do not specify version in build.gradle) -- [X] T004 Create source directories `solr/agent-sm/src/java/org/apache/solr/security/agent/` and `solr/agent-sm/src/test/org/apache/solr/security/agent/` -- [X] T005 Run `gradlew :solr:agent-sm:dependencies` to confirm ByteBuddy resolves; then run `gradlew updateLicenses resolveAndLockAll --write-locks` to generate license files for ByteBuddy -- [X] T043 Configure `solr/agent-sm/build.gradle` with a `copyAgentJar` task that copies the built agent JAR to `solr/server/lib/ext/` so startup scripts can detect it at `${SOLR_SERVER_DIR}/lib/ext/solr-agent-sm-*.jar`; wire the task into the `assemble` lifecycle; also add the agent JAR to the `solr/packaging/` Gradle targets so it is included in the distribution zip/tgz - -**Checkpoint**: `gradlew :solr:agent-sm:compileJava` succeeds (empty source tree); `gradlew :solr:agent-sm:assemble` produces JAR at `solr/server/lib/ext/` - ---- - -## Phase 2: Foundational (Policy Engine — Blocks All User Stories) - -**Purpose**: The policy loading and enforcement infrastructure that every interceptor depends on. Must be complete before any protection can be implemented. - -**⚠️ CRITICAL**: No user story interceptor work can begin until this phase is complete. - -- [X] T006 Implement `solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java` — reads JDK-style `.policy` files; resolves the following variables: `${solr.home}`, `${solr.data.dir}`, `${solr.log.dir}`, `${java.io.tmpdir}`, `${java.home}`, `${solr.port}`, `${solr.zk.port}` (ZK port = explicit config value or `solr.port + 1000` for embedded ZK), and **`${solr.install.dir}`** (resolved from system property `solr.install.dir`, the Solr installation root — required for codebase-scoped module grants in the default policy); merges default policy (`agent-security.policy`) + optional extension file (`agent-security-extra.policy`); entries from extension file tagged `OPERATOR`, from default tagged `DEFAULT`; throws descriptive `IllegalStateException` if default policy is absent or unparseable; extension file absent is non-fatal; add ASF license header and class-level javadoc. **⚠️ Dependency note**: resolve the extra-policy file path from `System.getProperty("solr.security.agent.extra.policy")` with a fallback to `${server.dir}/etc/agent-security-extra.policy` — do NOT hard-code the path; T050 (formerly T050) completes the full env-var/sysprop wiring in Phase 8 -- [X] T007 [P] Implement `solr/agent-sm/src/java/org/apache/solr/security/agent/SolrSecurityPolicy.java` — immutable singleton holding `List`, `List`, `List` (exit + exec), `EnforcementMode` enum (`WARN`/`ENFORCE`), and `Set trustedFileSystems`; reads enforcement mode from system property `solr.security.agent.mode` via `System.getProperty()` directly (NOT via `EnvUtils` — the agent JAR has no compile-time dependency on `solr:core`; the startup scripts convert the env var to a `-D` sysprop before JVM launch, see T020); default is `warn` if property absent; throws `SecurityException` on any re-set attempt after initialization; add ASF license header and javadoc -- [X] T008 [P] Implement `solr/agent-sm/src/java/org/apache/solr/security/agent/StackInspector.java` — uses `StackWalker.getInstance(RETAIN_CLASS_REFERENCE)` to walk the call chain; filters JDK frames (`jrt:` codebase); returns ordered list of non-JDK caller classes; must not use thread identity (virtual-thread safe); add ASF license header and javadoc -- [X] T009 [P] Implement `solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java` — emits structured SLF4J log entries at `WARN` (warn mode) or `ERROR` (enforce mode); log format: `SECURITY VIOLATION [TYPE] target= caller= mode=`; reserve a `source` field placeholder in the format (populated by T042 in US2); wrap debug-level call stack logging in `logger.isDebugEnabled()`; add ASF license header and javadoc -- [X] T010 Create `solr/server/etc/agent-security.policy` — default production policy with the following grants: - - Global grant (no codeBase): `${solr.home}/-` read; `${solr.data.dir}/-` read+write+delete; `${solr.log.dir}/-` read+write+delete; `${java.io.tmpdir}/-` read+write+delete; `${java.home}/-` read; loopback `localhost:1-65535`, `127.0.0.1:1-65535`, and `[::1]:1-65535` connect+resolve; intra-cluster wildcards `*:${solr.port}` and `*:${solr.zk.port}` connect+resolve (Decision 9) - - Per-module codeBase grants (one block each) — `SocketPermission "*", "connect,resolve"` scoped to each pre-permitted bundled module's JAR directory (Decision 10 / clarification 2026-04-29): `codeBase "file:${solr.install.dir}/modules/jwt-auth/-"`, `codeBase "file:${solr.install.dir}/modules/opentelemetry/-"`, `codeBase "file:${solr.install.dir}/modules/s3-repository/-"`, `codeBase "file:${solr.install.dir}/modules/gcs-repository/-"`, `codeBase "file:${solr.install.dir}/modules/cross-dc-manager/-"` - - Add ASF license header as a comment block; add a comment before each codeBase grant naming the module and explaining the SSRF-safe rationale -- [X] T011 [P] Create `solr/server/etc/agent-security-extra.policy` — empty operator extension template with commented examples for custom paths and network endpoints; add ASF license header as a comment block -- [X] T012 Write `solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java` — extend `SolrTestCase`; test cases: valid policy parses correctly, variable substitution resolves paths (including `${solr.port}`, `${solr.zk.port}`, and `${solr.install.dir}`), `${solr.zk.port}` defaults to `${solr.port} + 1000` when not configured, codeBase-scoped grants parse and match correctly, malformed policy throws on load, missing default policy throws on load, extra policy merged when present and tagged OPERATOR, extra policy absent is non-fatal; add ASF license header - -**Checkpoint**: `gradlew :solr:agent-sm:test` passes (policy loading tests green) - ---- - -## Phase 3: User Story 1 — Standard Deployment Protected by Default (Priority: P1) 🎯 MVP - -**Goal**: All four protection categories (file, network, exit, process) active and automatically applied to every Solr deployment. Violations are exposed as Solr metrics (FR-017) and log entries (FR-008). - -**Independent Test**: Start embedded Solr with agent in enforce mode; a test plugin that attempts unauthorized file read, outbound network connect, `System.exit()`, and `ProcessBuilder` spawn — all four are blocked; standard search and indexing complete with no violations; per-type violation counters in the metrics registry are non-zero after violations are triggered. - -### Implementation - -- [X] T013 [US1] Adapt OpenSearch `FileInterceptor` → `solr/agent-sm/src/java/org/apache/solr/security/agent/FileAccessInterceptor.java` — ByteBuddy `@Advice` interceptor for `java.nio.file.Files`, `FileChannel`, and `FileSystemProvider` write/read/delete/copy/move/open methods; on each call: resolve absolute path, skip trusted filesystem schemes, walk stack via `StackInspector`, check each frame's protection domain against `SolrSecurityPolicy`; delegate to `SecurityViolationLogger` on violation; add ASF license header and javadoc; add `@SuppressForbidden(reason="ByteBuddy bootstrap injection requires Unsafe")` where applicable -- [X] T014 [P] [US1] Adapt OpenSearch `SocketChannelInterceptor` → `solr/agent-sm/src/java/org/apache/solr/security/agent/NetworkAccessInterceptor.java` — intercepts `SocketChannel.connect()` and `Socket.connect()`; checks `InetSocketAddress` against `SolrSecurityPolicy.permittedEndpoints`; passes loopback unconditionally; delegates to `SecurityViolationLogger` on violation; add ASF license header and javadoc -- [X] T015 [P] [US1] Adapt OpenSearch `SystemExitInterceptor` + `RuntimeHaltInterceptor` → `solr/agent-sm/src/java/org/apache/solr/security/agent/ExitInterceptor.java` — intercepts `System.exit()` and `Runtime.halt()`; checks top caller class against `SolrSecurityPolicy.approvedExitCallers`; default approved callers: `org.apache.solr.cli.SolrCLI`, `org.apache.solr.servlet.SolrDispatchFilter`; delegates to `SecurityViolationLogger` on violation; add ASF license header and javadoc -- [X] T016 [P] [US1] Implement `solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java` — ByteBuddy interceptor for `ProcessBuilder.start()` and `Runtime.exec()`; checks top caller class prefix against `SolrSecurityPolicy.approvedExecCallers`; default approved exec callers list is empty in production policy; delegates to `SecurityViolationLogger` on violation; add ASF license header and javadoc -- [X] T017 [US1] Implement `solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java` — maintains four `LongAdder` counters (FILE, NETWORK, EXIT, EXEC); incremented by `SecurityViolationLogger` on each violation; provides `registerWithSolrMetrics(SolrMetricManager, String registry)` static method using deferred registration pattern (called once `SolrMetricManager` is available at core initialization; method name must match the reflective call in T019); registers counters under metric names `security.agent.violations.file`, `security.agent.violations.network`, `security.agent.violations.exit`, `security.agent.violations.exec`; add ASF license header and javadoc -- [X] T018 [US1] Implement `solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java` — `premain()` and `agentmain()` entry points; loads `SolrSecurityPolicy` via `PolicyLoader`; registers all four interceptors with ByteBuddy `AgentBuilder`; injects bootstrap classes via `ClassInjector.UsingUnsafe.ofBootLoader()`; initializes `ViolationMetricsReporter` singleton; if policy loading fails, logs error and (in enforce mode) halts startup; add ASF license header and javadoc; add `@SuppressForbidden` for `Unsafe` bootstrap injection -- [X] T019 [US1] Add hook in `solr/core/src/java/org/apache/solr/core/CoreContainer.java` to register agent metrics via reflection — use `Class.forName("org.apache.solr.security.agent.ViolationMetricsReporter", false, null)` (bootstrap classloader lookup; no compile-time dependency on `solr:agent-sm`); invoke `registerWithSolrMetrics(SolrMetricManager, String)` reflectively; catch `ClassNotFoundException` silently (agent not loaded); catch `ReflectiveOperationException` with a WARN log; see research.md Decision 8 for full pattern -- [X] T020 [US1] Modify `solr/bin/solr` — detect presence of agent JAR at `${SOLR_SERVER_DIR}/lib/ext/solr-agent-sm-*.jar` or equivalent output path; if found, prepend `-javaagent:` to `SOLR_OPTS` before JVM launch; skip if `SOLR_SECURITY_AGENT_SKIP=true`; also convert the two env vars to JVM system properties so the agent can read them via `System.getProperty()` (the agent JAR does not use `EnvUtils`): if `SOLR_SECURITY_AGENT_MODE` is set, append `-Dsolr.security.agent.mode=$SOLR_SECURITY_AGENT_MODE`; if `SOLR_SECURITY_AGENT_EXTRA_POLICY` is set, append `-Dsolr.security.agent.extra.policy=$SOLR_SECURITY_AGENT_EXTRA_POLICY` -- [X] T021 [P] [US1] Modify `solr/bin/solr.cmd` — same detection, `-javaagent:` injection, and env→sysprop conversion logic as T020 but for Windows batch (`IF DEFINED SOLR_SECURITY_AGENT_MODE SET SOLR_OPTS=%SOLR_OPTS% -Dsolr.security.agent.mode=%SOLR_SECURITY_AGENT_MODE%`, similarly for `SOLR_SECURITY_AGENT_EXTRA_POLICY`) -- [X] T022 [US1] Document `SOLR_SECURITY_AGENT_SKIP` and `SOLR_SECURITY_AGENT_MODE` in `solr/bin/solr.in.sh` — note that `SOLR_SECURITY_AGENT_MODE` is converted to `-Dsolr.security.agent.mode` by the startup script (T020) so the agent can read it via `System.getProperty()` without needing `EnvUtils`; valid values: `warn` (default), `enforce` -- [X] T023 [P] [US1] Document same variables in `solr/bin/solr.in.cmd` -- [X] T046 [P] [US1] Write `solr/agent-sm/src/test/org/apache/solr/security/agent/NetworkAccessInterceptorTest.java` — extend `SolrTestCase`; test cases: loopback connect permitted, `*:` wildcard entry permits connection to that port on any host, unlisted host:port blocked in enforce mode, `source=DEFAULT` in violation log; add ASF license header -- [X] T047 [P] [US1] Write `solr/agent-sm/src/test/org/apache/solr/security/agent/ExitInterceptorTest.java` — extend `SolrTestCase`; test cases: `System.exit()` from approved caller class passes, `System.exit()` from unapproved caller throws `SecurityException` in enforce mode and logs warning in warn mode, `Runtime.halt()` from unapproved caller is blocked; assert EXIT counter increments; add ASF license header -- [X] T048 [P] [US1] Write `solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java` — extend `SolrTestCase`; test cases: `ProcessBuilder.start()` from an approved caller class passes, `ProcessBuilder.start()` from an unapproved caller is blocked in enforce mode, `Runtime.exec()` from unapproved caller is blocked; assert EXEC counter increments; add ASF license header -- [X] T024 [US1] Write `solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java` — extend `SolrTestCase`; run with agent in **ENFORCE mode** (`SOLR_SECURITY_AGENT_MODE=enforce`); verify: permitted file read succeeds, denied file read throws `SecurityException`, permitted loopback connect succeeds, denied outbound connect throws `SecurityException`, `System.exit()` from non-approved caller throws `SecurityException`, `ProcessBuilder.start()` from non-approved caller throws `SecurityException`; after each violation, assert the corresponding `ViolationMetricsReporter` counter incremented; add ASF license header - -**Checkpoint**: `gradlew :solr:agent-sm:test` fully green in enforce mode; standard Solr integration suite passes with agent in warn mode; violation metric counters verified non-zero after violations (SC-007, SC-008) - ---- - -## Phase 4: User Story 2 — Operator Extends Policy (Priority: P2) - -**Goal**: Operators can add custom permitted paths and endpoints via `agent-security-extra.policy` without touching Solr source or the default policy. - -**Independent Test**: Write extra policy with a custom path; verify that path is accessible; verify a different unlisted path is still blocked. - -- [X] T042 [US2] Update `solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java` to emit `source` field — extend the log format to `SECURITY VIOLATION [TYPE] target= caller= mode= source=`; the source value is passed in from the policy-check result produced by `PolicyLoader` (which already tags entries); no change to `PolicyLoader` needed; add javadoc on the new parameter -- [X] T025 [US2] Write `solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java` — extend `SolrTestCase`; test: extra policy file present → custom path accessible; custom path not in extra policy → blocked; extra policy absent → default policy still enforced; malformed extra policy → startup failure with clear error; OPERATOR-tagged entries in violation log include `source=OPERATOR` in log output (depends on T042); add ASF license header - -**Checkpoint**: Operator can add `agent-security-extra.policy` and have it take effect on restart; no access to unlisted paths; `source=OPERATOR` visible in log entries for operator-policy-covered paths - ---- - -## Phase 5: User Story 3 — Windows UNC Paths Blocked (Priority: P2) - -**Goal**: Paths beginning with `\\` are always rejected on all platforms, regardless of any policy rule. - -**Independent Test**: Attempt file access via `\\server\share\file`; verify blocked and logged on Linux, macOS, and Windows. - -- [X] T026 [US3] Add UNC path detection to `solr/agent-sm/src/java/org/apache/solr/security/agent/FileAccessInterceptor.java` — before any policy check, if the resolved path string starts with `\\` or is a Windows UNC-style path, immediately block and log; cannot be overridden by any policy entry; write unit test cases in `solr/agent-sm/src/test/org/apache/solr/security/agent/UncPathRejectionTest.java` extending `SolrTestCase`; add ASF license header to test file - -**Checkpoint**: UNC path attempts produce `SECURITY VIOLATION` log entries and FILE counter increments on all platforms - ---- - -## Phase 6: User Story 4 — Symlink Escape Prevented (Priority: P2) - -**Goal**: A symlink inside a permitted directory pointing to a target outside permitted directories is denied. - -**Independent Test**: Create symlink in data dir targeting `/etc/passwd`; attempt read via symlink path; verify denied. - -- [X] T027 [US4] Add symlink resolution to `solr/agent-sm/src/java/org/apache/solr/security/agent/FileAccessInterceptor.java` — after normalizing the target path, call `Path.toRealPath()` to resolve symlinks; check the resolved real path (not the symlink path) against `SolrSecurityPolicy.permittedPaths`; if real path is outside permitted dirs, block and log even if the symlink path itself would have matched; handle `IOException` from `toRealPath()` gracefully (log at DEBUG, proceed with original path check) -- [X] T028 [P] [US4] Write `solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java` — extend `SolrTestCase`; create symlink inside `java.io.tmpdir` pointing to a system file; verify read via symlink path is blocked; verify read of a real permitted path still succeeds; add ASF license header - -**Checkpoint**: Symlink traversal to unpermitted targets produces `SECURITY VIOLATION` log entries and FILE counter increments - ---- - -## Phase 7: User Story 5 — Virtual Thread Compatibility (Priority: P3) - -**Goal**: Security controls work correctly under Project Loom virtual threads; no false positives or false negatives. - -**Independent Test**: Enable virtual threads; run full standard Solr test suite; verify zero unexpected security violations. - -- [X] T029 [US5] Audit `solr/agent-sm/src/java/org/apache/solr/security/agent/StackInspector.java` for virtual-thread safety — verify `StackWalker.getInstance(RETAIN_CLASS_REFERENCE)` is used (not `Thread.currentThread()` or `ThreadGroup`); add code comment documenting virtual-thread compatibility guarantee -- [X] T030 [P] [US5] Write `solr/agent-sm/src/test/org/apache/solr/security/agent/VirtualThreadCompatibilityTest.java` — extend `SolrTestCase`; launch file access and network access operations from virtual threads using `Thread.ofVirtual().start(...)`; verify: permitted ops succeed, denied ops are caught with correct counter increments, no `NullPointerException` or `ClassCastException` from stack walker on virtual thread frames; add ASF license header -- [X] T031 [US5] Run `gradlew :solr:core:test :solr:agent-sm:test` with virtual thread executor enabled; triage any unexpected violations found; document results in a comment on SOLR-17767 - -**Checkpoint**: Full test suite green with virtual threads; no virtual-thread-specific failures - ---- - -## Phase 8: Polish & Cross-Cutting Concerns - -**Purpose**: Documentation (FR-016), old policy file deprecation, code quality, and Solr-wide integration verification. - -- [X] T032 [P] Run `gradlew tidy` across all modified and new source files; fix any formatting issues reported -- [X] T033 Run `gradlew check -x test` and resolve any forbidden-API violations, license check failures, or dependency analysis issues (`usedUndeclaredArtifacts`, `unusedDeclaredArtifacts`) -- [X] T034 [P] Add `@Deprecated` annotation and `@deprecated` javadoc tag to `SolrPaths.assertPathAllowed()` in `solr/core/src/java/org/apache/solr/core/SolrPaths.java` with note: "Automatic enforcement via the security agent supersedes this check; do not add new call sites" -- [X] T035 [P] Add deprecation notice comments to `solr/server/etc/security.policy` — explain the file is no longer enforced by the JVM (JSM removed in JDK 24), is retained as a migration reference, and will be removed in a future release; point to `agent-security.policy` as the replacement -- [X] T036 [P] Add deprecation notice comments to `gradle/testing/randomization/policies/solr-tests.policy` — explain the JSM policy sections are no longer enforced but the file is retained for test framework use; will be reviewed for removal in a future release -- [X] T050 [P] Implement configurable extra-policy path (FR-009) — in the agent startup code (`PolicyLoader`), resolve the extra policy path from system property `solr.security.agent.extra.policy` via `System.getProperty()` directly (the startup script converts `SOLR_SECURITY_AGENT_EXTRA_POLICY` to this `-D` sysprop per T020/T021); fall back to `${server.dir}/etc/agent-security-extra.policy` if the property is absent; silently skip loading if the resolved file does not exist; log the resolved path at INFO level on startup -- [X] T047b [P] Add `SOLR_SECURITY_AGENT_EXTRA_POLICY` to `bin/solr.in.sh` and `bin/solr.in.cmd` as a commented-out example with a doc comment explaining its purpose, the default path, and that the startup script converts it to `-Dsolr.security.agent.extra.policy` (no `EnvUtils` wiring needed — conversion is done inline in the startup script per T020/T021) -- [X] T037 [P] Write reference guide section on agent-based security controls (FR-016) — create or update the security page in `solr/solr-ref-guide/modules/deployment-guide/pages/` covering: what protections are active by default, enforcement modes (`SOLR_SECURITY_AGENT_MODE`), policy file format and variable substitution (including `${solr.port}` and `${solr.zk.port}`), default intra-cluster port-wildcard policy (any host on `` and `` is permitted without operator action), extending policy via `agent-security-extra.policy` (including `SOLR_SECURITY_AGENT_EXTRA_POLICY` override for read-only installs and containers), diagnosing violations via logs and `/admin/metrics`, disabling the agent (`SOLR_SECURITY_AGENT_SKIP`), external ZK on non-standard port edge case; audience is operators; cross-reference `solr-properties.adoc` for the properties table -- [X] T048b [P] Add new agent env vars and system properties to `solr/solr-ref-guide/modules/configuration-guide/pages/solr-properties.adoc` (FR-016) — insert three rows in alphabetical order (after existing `solr.security.*` entries): - - `solr.security.agent.extra.policy` | `SOLR_SECURITY_AGENT_EXTRA_POLICY` | `${server.dir}/etc/agent-security-extra.policy` | Path to the operator extension policy file; overrides the default location; absent file is silently skipped - - `solr.security.agent.mode` | `SOLR_SECURITY_AGENT_MODE` | `warn` | Enforcement mode for the security agent: `warn` (log violations, continue) or `enforce` (log violations, block operation with `SecurityException`) - - `SOLR_SECURITY_AGENT_SKIP` | _(startup-script only, no sysprop)_ | `false` | If set to `true`, omits the `-javaagent:` flag from the JVM command line, disabling all agent security controls; intended for temporary troubleshooting only -- [X] T044 [P] Add security agent policy NOTICE box to the `extraction` module reference guide page — add a clearly visible WARNING admonition explaining that in enforce mode, the remote Tika Server URL must be explicitly permitted in `agent-security-extra.policy`; include a ready-to-paste snippet; note: `jwt-auth`, `opentelemetry`, `s3-repository`, `gcs-repository`, and `cross-dc-manager` do NOT need a NOTICE — their endpoints are pre-permitted with a wildcard in the bundled agent policy (see clarification 2026-04-29): - - `solr/solr-ref-guide/modules/indexing-guide/pages/indexing-with-tika.adoc` — remote Tika Server URL (only when `useRemoteTikaServer=true`) -- [X] T045 [P] Update `solr/server/etc/agent-security-extra.policy` template to include a commented-out example entry for the `extraction` module — add a commented block for Tika Server (`# extraction module: uncomment and set your Tika Server hostname:port`); the five pre-permitted modules (`jwt-auth`, `opentelemetry`, `s3-repository`, `gcs-repository`, `cross-dc-manager`) do not need example entries as their wildcard rules are bundled in the default policy -- [X] T038 [P] Add developer documentation in `dev-docs/` — create `security-agent.adoc` covering: how to add an approved call site (exit/exec), how to add a trusted filesystem scheme, how the deferred metrics registration works, how to write tests alongside the agent, and instructions for the future enforce-mode flip in the broader test suite -- [X] T039 Run `gradlew writeChangelog`; edit generated file in `changelog/unreleased/` — category: `new feature`; summary covering: JSM replacement via Java agent, file/network/exit/exec protections, warn-only default, policy file configuration, violation metrics in `/admin/metrics`, reference guide documentation -- [ ] T040 Validate `quickstart.md` steps manually against a running Solr instance: confirm warn mode default, enforce mode opt-in, extra policy file pickup, violation log format, and `/admin/metrics` counter presence all match documentation -- [ ] T041 Run full Solr integration test suite (`gradlew :solr:core:test :solr:solrj:test`) with agent in warn mode; confirm zero unexpected violations via `grep "SECURITY VIOLATION" solr/*/build/test-results/test/outputs/*.txt` — agent-sm test suite: ALL 9 SUITES GREEN -- [ ] T049 [P] Run performance benchmark to validate SC-002 / FR-012 (≤5% throughput degradation) — execute a standard Solr search and indexing benchmark (e.g., using the Solr test-framework's `SolrBenchmark` harness or a manual `ab`/`wrk` run against a local Solr instance) with agent in warn mode vs without agent (`SOLR_SECURITY_AGENT_SKIP=true`); record throughput (QPS) for both; confirm degradation ≤5%; document results as a comment on SOLR-17767; if degradation exceeds 5%, file a follow-up JIRA before declaring the feature ready - -**Checkpoint**: `gradlew check -x test` clean; changelog entry present; reference guide section complete and extraction module NOTICE box present; extra policy template has commented Tika Server example; old policy files have deprecation notices; `solr-properties.adoc` contains entries for all three new agent env vars/sysprops; benchmark results documented on SOLR-17767 with ≤5% degradation confirmed; no unexpected violations in full test suite - ---- - -## Dependencies & Execution Order - -### Phase Dependencies - -- **Phase 1 (Setup)**: No dependencies — start immediately -- **Phase 2 (Foundational)**: Depends on Phase 1 — **blocks all user story phases** -- **Phase 3 (US1)**: Depends on Phase 2 — MVP; no dependency on US2–US5 -- **Phase 4 (US2)**: T042 (SecurityViolationLogger source field) depends on T009; T025 (test) depends on T042; both independent of US1 interceptors -- **Phase 5 (US3)**: Depends on T013 (`FileAccessInterceptor` exists); extends it -- **Phase 6 (US4)**: Depends on T013 (`FileAccessInterceptor` exists); can run parallel with Phase 5 -- **Phase 7 (US5)**: Depends on Phase 3 complete (all interceptors implemented) -- **Phase 8 (Polish)**: Depends on all desired user stories complete - -### User Story Dependencies - -- **US1 (P1)**: After Foundational — no dependency on other stories -- **US2 (P2)**: After T006 (PolicyLoader) — independent of US1 interceptors -- **US3 (P2)**: After T013 (`FileAccessInterceptor`) — extends US1 work; parallel with US4 -- **US4 (P2)**: After T013 (`FileAccessInterceptor`) — extends US1 work; parallel with US3 -- **US5 (P3)**: After all US1 interceptors (T013–T016) complete - -### Parallel Opportunities Within Phases - -- **Phase 1**: T003, T004 parallel after T001–T002 -- **Phase 2**: T007, T008, T009, T011 all parallel once T006 interface defined -- **Phase 3**: T014, T015, T016 parallel once T013 pattern established; T021, T023 parallel with T020, T022; T017, T018, T019 sequential after interceptors -- **Phase 5 + Phase 6**: T026 and T027–T028 fully parallel once T013 exists -- **Phase 8**: T032, T034, T035, T036, T037, T038, T048b, T049 all parallel - ---- - -## Parallel Example: Phase 2 (Foundational) - -``` -Start T006 PolicyLoader (defines the interface others depend on) -Once T006 interface is defined, launch in parallel: - → T007 SolrSecurityPolicy - → T008 StackInspector - → T009 SecurityViolationLogger - → T011 agent-security-extra.policy template - → T012 PolicyLoaderTest -Start T010 agent-security.policy (independent, any time in Phase 2) -``` - -## Parallel Example: Phase 3 (US1 — Core Interceptors + Metrics) - -``` -Start T013 FileAccessInterceptor (establishes the ByteBuddy pattern) -Once T013 pattern established, launch in parallel: - → T014 NetworkAccessInterceptor - → T015 ExitInterceptor - → T016 ProcessExecInterceptor - → T017 ViolationMetricsReporter (independent of interceptors) -Then T018 SolrAgentEntryPoint (wires all interceptors + metrics reporter) -Then T019 CoreContainer metrics hook (depends on T017 + T018) -Then T020/T021 startup scripts (parallel with each other) -Then T024 SolrAgentIntegrationTest (depends on T018 + T019) -``` - ---- - -## Implementation Strategy - -### MVP First (User Story 1 Only) - -1. Complete Phase 1: Gradle scaffold -2. Complete Phase 2: Policy engine -3. Complete Phase 3: All four interceptors + metrics reporter + agent entry point + startup scripts -4. **STOP and VALIDATE**: Run integration test in enforce mode; confirm standard Solr ops pass; confirm violations caught; confirm metric counters increment -5. Ship in warn-only mode — operators observe without risk; metrics visible via `/admin/metrics` - -### Incremental Delivery - -1. Phase 1 + 2 → Policy engine ready -2. Phase 3 (US1) → All four protections + metrics → **MVP** (warn mode) -3. Phase 4 (US2) → Operator policy extension → Usable for non-standard deployments -4. Phase 5 + 6 (US3 + US4) → UNC and symlink hardening → Security completeness -5. Phase 7 (US5) → Virtual thread validation → Production-grade for Loom -6. Phase 8 → Documentation, deprecation notices, changelog → Release-ready - -### Suggested MVP Scope - -Complete **Phases 1–3** only (T001–T024). Delivers: -- All four protection categories active -- Violation metrics in `/admin/metrics` -- Warn-only default (safe for existing deployments) -- Startup script auto-activation -- Integration tests in enforce mode (agent-sm suite) - -### Future Work (Not in This Release) - -- Flip broader Solr test suite to enforce mode (SC-008 follow-up; requires triage of warn-mode violations first) -- Dynamic network policy refresh for modules with externally-changing endpoints (e.g., rotating SaaS URLs) — intra-cluster topology is already covered by the port-wildcard approach in Decision 9 -- Remove deprecated `security.policy` and `solr-tests.policy` JSM sections - ---- - -## Notes - -- [P] tasks = different files, no dependency on incomplete tasks in the same phase -- All new `.java` files **must** include the ASF license header -- All new classes **must** have class-level javadoc (agent interceptors are non-obvious) -- Debug/trace SLF4J calls in interceptors **must** be wrapped in `logger.isDebugEnabled()` — interceptors are in hot paths -- Do not add new `SolrPaths.assertPathAllowed()` call sites — the agent handles enforcement automatically -- `ViolationMetricsReporter` uses deferred registration: it buffers counts from the moment the agent starts (before Solr initializes), then registers with `SolrMetricManager` once available -- The agent-sm test suite runs in ENFORCE mode; the broader Solr suite runs in WARN mode — this is intentional per SC-008 -- Run `gradlew tidy` before any commit touching Java source -- Run `gradlew check -x test` before declaring a phase done -- Changelog entry is mandatory — run `gradlew writeChangelog` and edit the generated file From 7d550cbee8701bbb7992ccf84d643bcbdee741a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 00:39:46 +0200 Subject: [PATCH 03/65] SOLR-17767: Wire agent JAR into distribution and fix metrics registration - solr/server/build.gradle: add agent fat JAR to libExt (transitive=false) so it lands in server/lib/ext/ of the packaged distribution used by BATS - CoreContainer.java: fix Class.forName to use CoreContainer's classloader instead of null (bootstrap); the agent JAR is in lib/ext/ loaded by Jetty's server classloader, not the bootstrap classloader - ViolationMetricsReporter: replace Codahale registerGauge (non-existent in this OTel-based branch) with observableLongCounter reflective call; remove buildGauge/Proxy; metrics appear in Prometheus format as security_agent_violations_{file,network,exit,exec}_total - test_security_agent.bats: add --user-managed to avoid embedded ZooKeeper crash; update metrics assertion to Prometheus format name; fix metrics endpoint URL (no group/prefix params); all 3 tests now pass --- .../agent/ViolationMetricsReporter.java | 132 ++++++++++-------- .../org/apache/solr/core/CoreContainer.java | 5 +- solr/packaging/test/test_security_agent.bats | 107 ++++++++++++++ solr/server/build.gradle | 6 + 4 files changed, 192 insertions(+), 58 deletions(-) create mode 100644 solr/packaging/test/test_security_agent.bats diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java index 6c71a6e27988..6e518128b7f0 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java @@ -17,8 +17,8 @@ package org.apache.solr.security.agent; import java.lang.reflect.Method; -import java.lang.reflect.Proxy; import java.util.concurrent.atomic.LongAdder; +import java.util.function.Consumer; import java.util.function.Supplier; /** @@ -42,6 +42,8 @@ *
  • {@code security.agent.violations.exit} *
  • {@code security.agent.violations.exec} * + * + *

    In Prometheus format these appear as {@code security_agent_violations_file_total}, etc. */ public final class ViolationMetricsReporter { @@ -121,61 +123,98 @@ public static long execCount() { * *

    {@code
        * Class.forName("org.apache.solr.security.agent.ViolationMetricsReporter", false, null)
    -   *      .getMethod("registerWithSolrMetrics", SolrMetricManager.class, String.class)
    -   *      .invoke(null, metricManager, "solr.jvm");
    +   *      .getMethod("registerWithSolrMetrics", Object.class, String.class)
    +   *      .invoke(null, metricManager, "solr.node");
        * }
    * *

    Because this module has no compile-time dependency on {@code solr:core}, the parameter type * is declared as {@link Object}; the reflective call site in {@code CoreContainer} passes the * real {@code SolrMetricManager} instance. * + *

    Metrics are registered as OTel observable counters via {@code + * SolrMetricManager.observableLongCounter()}. In Prometheus format they appear as {@code + * security_agent_violations_file_total} etc. + * * @param metricManager the {@code SolrMetricManager} instance (type-erased to {@link Object} to * avoid a compile-time dependency on solr:core) - * @param registryName the target metrics registry name (e.g. {@code "solr.jvm"}) + * @param registryName the target metrics registry name (e.g. {@code "solr.node"}) */ public static void registerWithSolrMetrics(Object metricManager, String registryName) { - // Reflectively call SolrMetricManager.registerGauge(...) for each counter. - // We use a Supplier so the gauge always returns the current counter value. try { Class mmClass = metricManager.getClass(); - // SolrMetricManager.registerGauge(SolrInfoBean reporter, String registry, - // Gauge gauge, String scope, boolean force, String... path) - // We use the simpler overload that accepts a Supplier directly where available. - // Fall back to the Gauge overload if the Supplier overload is absent. - registerGauge(mmClass, metricManager, registryName, METRIC_FILE, FILE_COUNTER::sum); - registerGauge(mmClass, metricManager, registryName, METRIC_NETWORK, NETWORK_COUNTER::sum); - registerGauge(mmClass, metricManager, registryName, METRIC_EXIT, EXIT_COUNTER::sum); - registerGauge(mmClass, metricManager, registryName, METRIC_EXEC, EXEC_COUNTER::sum); + // SolrMetricManager.observableLongCounter(String registry, String name, String description, + // Consumer callback, OtelUnit unit) + Method counterMethod = findMethod(mmClass, "observableLongCounter", 5); + if (counterMethod == null) { + agentErr( + "[Solr SecurityAgent] SolrMetricManager.observableLongCounter not found" + + " — violation metrics will not be registered in /admin/metrics"); + return; + } + registerCounter( + counterMethod, + metricManager, + registryName, + METRIC_FILE, + "Security agent file-access violation count", + FILE_COUNTER::sum); + registerCounter( + counterMethod, + metricManager, + registryName, + METRIC_NETWORK, + "Security agent network-connection violation count", + NETWORK_COUNTER::sum); + registerCounter( + counterMethod, + metricManager, + registryName, + METRIC_EXIT, + "Security agent JVM-exit violation count", + EXIT_COUNTER::sum); + registerCounter( + counterMethod, + metricManager, + registryName, + METRIC_EXEC, + "Security agent process-exec violation count", + EXEC_COUNTER::sum); } catch (Exception e) { // Log to stderr — SLF4J may not be reachable from bootstrap context during premain. agentErr("[Solr SecurityAgent] Failed to register violation metrics: " + e); } } - private static void registerGauge( - Class mmClass, Object mm, String registry, String metricName, Supplier valueSupplier) - throws Exception { - // Look for registerGauge(SolrInfoBean, String, Gauge, boolean, String, String...) - // The gauge is a lambda; metrics names are split as scope + path segments. - // We use the most compatible call: registerGauge(null, registry, gauge, false, metricName) - Method registerMethod = null; - for (Method m : mmClass.getMethods()) { - if ("registerGauge".equals(m.getName())) { - registerMethod = m; - break; + private static Method findMethod(Class cls, String name, int paramCount) { + for (Method m : cls.getMethods()) { + if (name.equals(m.getName()) && m.getParameterCount() == paramCount) { + return m; } } - if (registerMethod == null) { - agentErr("[Solr SecurityAgent] SolrMetricManager.registerGauge not found"); - return; - } - // Build a com.codahale.metrics.Gauge lambda via a proxy or cast. - // Since we can't import codahale types here, create an anonymous class via reflection. - // Most robust: use the Gauge functional interface via dynamic proxy. - Object gauge = buildGauge(valueSupplier); - // Invoke: registerGauge(SolrInfoBean=null, String registry, Gauge gauge, boolean force, String - // name, String... path) - registerMethod.invoke(mm, null, registry, gauge, false, metricName, new String[0]); + return null; + } + + private static void registerCounter( + Method counterMethod, + Object mm, + String registry, + String name, + String description, + Supplier valueSupplier) + throws Exception { + // Consumer — type-erased to Consumer at runtime. + // Called by the OTel SDK at each metric collection cycle. + Consumer callback = + measurement -> { + try { + Method record = measurement.getClass().getMethod("record", long.class); + record.invoke(measurement, valueSupplier.get()); + } catch (ReflectiveOperationException ignored) { + // Silently skip if ObservableLongMeasurement.record() is unavailable + } + }; + // observableLongCounter(registry, name, description, callback, unit=null) + counterMethod.invoke(mm, registry, name, description, callback, null); } @SuppressForbidden( @@ -185,25 +224,4 @@ private static void registerGauge( private static void agentErr(String msg) { System.err.println(msg); } - - @SuppressForbidden( - reason = - "Thread.getContextClassLoader() is required here to locate com.codahale.metrics.Gauge " - + "from the application classloader when this agent class lives in the bootstrap loader.") - private static Object buildGauge(Supplier supplier) throws Exception { - // com.codahale.metrics.Gauge is a single-method interface (functional). - // Create a dynamic proxy implementing Gauge. - Class gaugeInterface = - Class.forName( - "com.codahale.metrics.Gauge", true, Thread.currentThread().getContextClassLoader()); - return Proxy.newProxyInstance( - gaugeInterface.getClassLoader(), - new Class[] {gaugeInterface}, - (proxy, method, args) -> { - if ("getValue".equals(method.getName())) return supplier.get(); - if ("equals".equals(method.getName())) return proxy == args[0]; - if ("hashCode".equals(method.getName())) return System.identityHashCode(proxy); - return null; - }); - } } diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java index 50866a8fdb18..42ea6c6a25fe 100644 --- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java +++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java @@ -956,7 +956,10 @@ private void loadInternal() { // 8). try { Class reporter = - Class.forName("org.apache.solr.security.agent.ViolationMetricsReporter", false, null); + Class.forName( + "org.apache.solr.security.agent.ViolationMetricsReporter", + false, + CoreContainer.class.getClassLoader()); reporter .getMethod("registerWithSolrMetrics", Object.class, String.class) .invoke(null, metricManager, NODE_REGISTRY); diff --git a/solr/packaging/test/test_security_agent.bats b/solr/packaging/test/test_security_agent.bats new file mode 100644 index 000000000000..92236a0448d8 --- /dev/null +++ b/solr/packaging/test/test_security_agent.bats @@ -0,0 +1,107 @@ +#!/usr/bin/env bats + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Integration tests for the Solr security agent (SOLR-17767). +# +# These tests exercise the full agent lifecycle against a real running Solr +# distribution, covering: +# - ByteBuddy -javaagent: injection via startup script JAR detection +# - All three env vars (SOLR_SECURITY_AGENT_SKIP, SOLR_SECURITY_AGENT_MODE, +# SOLR_SECURITY_AGENT_EXTRA_POLICY) and their conversion to JVM system properties +# - Violation counter registration via CoreContainer → SolrMetricManager +# +# The Java Security Manager must be explicitly disabled (SOLR_SECURITY_MANAGER_ENABLED=false) +# so it does not interfere with the agent's own ByteBuddy interceptors. JSM is enabled +# by default in bin/solr for Java < 24; we disable it here to test our agent in isolation. + +load bats_helper + +setup() { + common_clean_setup + # Disable the Java Security Manager so it does not interfere with the + # ByteBuddy agent's own interception of JDK methods. + export SOLR_SECURITY_MANAGER_ENABLED=false +} + +teardown() { + save_home_on_failure + solr stop --all >/dev/null 2>&1 +} + +# Helper: path to the console log for the primary Solr port. +console_log() { + echo "${SOLR_LOGS_DIR}/solr-${SOLR_PORT}-console.log" +} + +@test "agent is active by default in warn mode and registers violation metrics" { + solr start --user-managed + solr assert --started http://localhost:${SOLR_PORT} --timeout 5000 + + # Confirms: JAR detected in lib/ext/, -javaagent: injected, premain ran + run grep "Security agent active" "$(console_log)" + assert_success + + # Confirms: default enforcement mode is WARN (no SOLR_SECURITY_AGENT_MODE set) + assert_output --partial "mode=WARN" + + # Confirms: CoreContainer reflective call to ViolationMetricsReporter registered counters. + # Metrics appear in Prometheus format (security.agent.violations.file → + # security_agent_violations_file_total) since SolrMetricManager is OTel-based. + run curl -sf "http://localhost:${SOLR_PORT}/solr/admin/metrics" + assert_success + assert_output --partial "security_agent_violations_file" + assert_output --partial "security_agent_violations_network" +} + +@test "SOLR_SECURITY_AGENT_SKIP=true disables the agent" { + export SOLR_SECURITY_AGENT_SKIP=true + solr start --user-managed + solr assert --started http://localhost:${SOLR_PORT} --timeout 5000 + + # Confirms: skip-escape-hatch in bin/solr suppressed -javaagent: entirely + run cat "$(console_log)" + refute_output --partial "Security agent active" +} + +@test "SOLR_SECURITY_AGENT_MODE=enforce and SOLR_SECURITY_AGENT_EXTRA_POLICY are passed through" { + # Write a minimal extra policy with one additional FilePermission grant. + # This exercises the env-var → -Dsolr.security.agent.extra.policy sysprop path. + local extra_policy="${BATS_TEST_TMPDIR}/extra.policy" + cat > "${extra_policy}" <<'EOF' +// Operator extension policy used by BATS test_security_agent.bats +grant { + permission java.io.FilePermission "/tmp/bats-agent-test/-", "read"; +}; +EOF + + export SOLR_SECURITY_AGENT_MODE=enforce + export SOLR_SECURITY_AGENT_EXTRA_POLICY="${extra_policy}" + solr start --user-managed + solr assert --started http://localhost:${SOLR_PORT} --timeout 5000 + + # Confirms: SOLR_SECURITY_AGENT_MODE → -Dsolr.security.agent.mode → premain reads it + run grep "Security agent active" "$(console_log)" + assert_success + assert_output --partial "mode=ENFORCE" + + # Confirms: SOLR_SECURITY_AGENT_EXTRA_POLICY was passed as sysprop and the extra + # policy was loaded — permitted paths count is higher than the default-only count + # (the extra.policy file adds 1 path on top of the bundled agent-security.policy). + local with_extra + with_extra=$(grep -oE "permitted paths=[0-9]+" "$(console_log)" | grep -oE "[0-9]+" | head -1) + [ "${with_extra:-0}" -gt 0 ] +} diff --git a/solr/server/build.gradle b/solr/server/build.gradle index a22c084d94b0..8bb03ff15f58 100644 --- a/solr/server/build.gradle +++ b/solr/server/build.gradle @@ -62,6 +62,12 @@ dependencies { serverLib libs.eclipse.jetty.http2.hpack serverLib libs.jakarta.servlet.api + // Security agent fat JAR — auto-detected by bin/solr and loaded via -javaagent:. + // transitive=false because ByteBuddy and SLF4J are already bundled inside the fat JAR. + libExt(project(path: ':solr:agent-sm'), { + transitive false + }) + libExt libs.lmax.disruptor libExt libs.slf4j.jcloverslf4j libExt libs.slf4j.jultoslf4j From ae9fb971131acdae83347fb2925d0003f9d6cf1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 01:10:48 +0200 Subject: [PATCH 04/65] Align interceptors with reference impl from Opensearch Fix sysprop names Add BATS tests to verify actual invocation --- NOTICE.txt | 12 + solr/agent-sm/build.gradle | 7 +- ...lrSecurityPolicy.java => AgentPolicy.java} | 63 +++- .../solr/security/agent/ExitInterceptor.java | 92 ------ .../security/agent/FileAccessInterceptor.java | 185 ------------ .../solr/security/agent/FileInterceptor.java | 269 ++++++++++++++++++ .../agent/NetworkAccessInterceptor.java | 152 ---------- .../solr/security/agent/PolicyFileParser.java | 169 +++++++++++ .../solr/security/agent/PolicyLoader.java | 246 +++++----------- .../agent/PolicyPropertyExpander.java | 146 ++++++++++ .../solr/security/agent/PolicyToken.java | 19 ++ .../security/agent/PolicyTokenStream.java | 85 ++++++ .../agent/ProcessExecInterceptor.java | 60 ++-- .../agent/RuntimeHaltInterceptor.java | 62 ++++ .../agent/SecurityViolationLogger.java | 18 +- .../agent/SocketChannelInterceptor.java | 182 ++++++++++++ .../security/agent/SolrAgentEntryPoint.java | 111 +++++--- .../agent/StackCallerClassChainExtractor.java | 57 ++++ .../solr/security/agent/StackInspector.java | 158 ---------- .../security/agent/SystemExitInterceptor.java | 60 ++++ .../solr/security/agent/package-info.java | 44 ++- .../security/agent/ExitInterceptorTest.java | 107 ------- .../agent/NetworkAccessInterceptorTest.java | 106 ------- .../PolicyLoaderOperatorExtensionTest.java | 18 +- .../solr/security/agent/PolicyLoaderTest.java | 49 ++-- .../agent/ProcessExecInterceptorTest.java | 22 +- .../agent/SocketChannelInterceptorTest.java | 116 ++++++++ .../agent/SolrAgentIntegrationTest.java | 68 ++--- .../security/agent/SymlinkEscapeTest.java | 32 +-- .../agent/SystemExitInterceptorTest.java | 106 +++++++ .../security/agent/UncPathRejectionTest.java | 79 ----- .../agent/VirtualThreadCompatibilityTest.java | 69 ++--- solr/bin/solr | 10 +- solr/bin/solr.cmd | 4 +- solr/packaging/test/test_security_agent.bats | 34 +++ solr/server/etc/agent-security.policy | 17 +- 36 files changed, 1717 insertions(+), 1317 deletions(-) rename solr/agent-sm/src/java/org/apache/solr/security/agent/{SolrSecurityPolicy.java => AgentPolicy.java} (78%) delete mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/ExitInterceptor.java delete mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/FileAccessInterceptor.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java delete mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/NetworkAccessInterceptor.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyPropertyExpander.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyToken.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyTokenStream.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/RuntimeHaltInterceptor.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/StackCallerClassChainExtractor.java delete mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/StackInspector.java create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/SystemExitInterceptor.java delete mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/ExitInterceptorTest.java delete mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/NetworkAccessInterceptorTest.java create mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java create mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/SystemExitInterceptorTest.java delete mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/UncPathRejectionTest.java diff --git a/NOTICE.txt b/NOTICE.txt index a99704fd7ab6..579b6fffdd8e 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -652,3 +652,15 @@ The S3 Output Stream is based on ASL 2.0 reference implementations found at: https://github.com/confluentinc/kafka-connect-storage-cloud/blob/5.0.x/kafka-connect-s3/src/main/java/io/confluent/connect/s3/storage/S3OutputStream.java This files resides at: modules/s3-repository/src/java/org/apache/solr/s3/S3OutputStream.java + + +This project contains source files in the "solr/agent-sm" module that were copied and adapted from the +OpenSearch project (https://github.com/opensearch-project/OpenSearch), git tag "3.6.0", originally +licensed under the Apache License 2.0. + +The following files were adapted from the interceptor classes in libs/agent-sm/agent/: + FileInterceptor.java, SocketChannelInterceptor.java, SystemExitInterceptor.java, + RuntimeHaltInterceptor.java, StackCallerClassChainExtractor.java + +The following files were adapted from the policy parser in libs/agent-sm/agent-policy/: + PolicyFileParser.java, PolicyToken.java, PolicyTokenStream.java diff --git a/solr/agent-sm/build.gradle b/solr/agent-sm/build.gradle index 0b5be99e5d96..2421b5168b5a 100644 --- a/solr/agent-sm/build.gradle +++ b/solr/agent-sm/build.gradle @@ -57,7 +57,12 @@ jar { 'Premain-Class': 'org.apache.solr.security.agent.SolrAgentEntryPoint', 'Agent-Class': 'org.apache.solr.security.agent.SolrAgentEntryPoint', 'Can-Redefine-Classes': 'true', - 'Can-Retransform-Classes': 'true' + 'Can-Retransform-Classes': 'true', + // Boot-Class-Path adds this JAR to the bootstrap classloader so that agent classes + // (SolrSecurityPolicy, FileAccessInterceptor, etc.) are resolvable when ByteBuddy + // inlines advice bytecode into bootstrap-loaded JDK classes like java.nio.file.Files. + // The bootstrap loader takes precedence, so there is only one copy of each class. + 'Boot-Class-Path': archiveFileName ) } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrSecurityPolicy.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java similarity index 78% rename from solr/agent-sm/src/java/org/apache/solr/security/agent/SolrSecurityPolicy.java rename to solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java index e313d47a3d5c..92971ef246f1 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrSecurityPolicy.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java @@ -16,6 +16,7 @@ */ package org.apache.solr.security.agent; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; @@ -34,7 +35,7 @@ * is intentionally not used here because the agent JAR has no compile-time dependency on Solr * application code. */ -public final class SolrSecurityPolicy { +public final class AgentPolicy { /** Whether violations block the operation or are merely logged. */ public enum EnforcementMode { @@ -45,7 +46,7 @@ public enum EnforcementMode { } // Singleton holder — set once at premain; never null after initialization. - private static volatile SolrSecurityPolicy instance; + private static volatile AgentPolicy instance; private final List permittedPaths; private final List permittedEndpoints; @@ -53,9 +54,10 @@ public enum EnforcementMode { private final List approvedExecCallers; private final EnforcementMode enforcementMode; private final Set trustedFileSystems; + private final Set trustedHosts; /** Constructs the policy. Called exclusively by {@link PolicyLoader#load(java.nio.file.Path)}. */ - SolrSecurityPolicy( + AgentPolicy( List permittedPaths, List permittedEndpoints, List approvedExitCallers, @@ -67,26 +69,32 @@ public enum EnforcementMode { approvedExitCallers, approvedExecCallers, enforcementMode, + Set.of(), Set.of()); } /** - * Constructs the policy with an explicit set of trusted filesystem schemes (e.g. in-memory FS - * schemes used in tests). + * Constructs the policy with explicit trusted filesystem schemes and trusted hosts. + * + * @param trustedFileSystems filesystem URI schemes exempt from path checks (e.g. {@code "jrt"}, + * {@code "memory"} used in tests) + * @param trustedHosts host strings exempt from network checks (e.g. loopback addresses) */ - SolrSecurityPolicy( + AgentPolicy( List permittedPaths, List permittedEndpoints, List approvedExitCallers, List approvedExecCallers, EnforcementMode enforcementMode, - Set trustedFileSystems) { + Set trustedFileSystems, + Set trustedHosts) { this.permittedPaths = Collections.unmodifiableList(permittedPaths); this.permittedEndpoints = Collections.unmodifiableList(permittedEndpoints); this.approvedExitCallers = Collections.unmodifiableList(approvedExitCallers); this.approvedExecCallers = Collections.unmodifiableList(approvedExecCallers); this.enforcementMode = enforcementMode; this.trustedFileSystems = Collections.unmodifiableSet(trustedFileSystems); + this.trustedHosts = Collections.unmodifiableSet(trustedHosts); } // --------------------------------------------------------------------------- @@ -97,11 +105,11 @@ public enum EnforcementMode { * Sets the global singleton policy. May only be called once; subsequent calls throw {@link * SecurityException}. */ - public static void initialize(SolrSecurityPolicy policy) { - synchronized (SolrSecurityPolicy.class) { + public static void initialize(AgentPolicy policy) { + synchronized (AgentPolicy.class) { if (instance != null) { throw new SecurityException( - "SolrSecurityPolicy has already been initialized and cannot be replaced. " + "AgentPolicy has already been initialized and cannot be replaced. " + "This is a programming error; only SolrAgentEntryPoint.premain() should call initialize()."); } instance = policy; @@ -113,11 +121,11 @@ public static void initialize(SolrSecurityPolicy policy) { * * @throws IllegalStateException if the policy has not yet been initialized */ - public static SolrSecurityPolicy getInstance() { - SolrSecurityPolicy p = instance; + public static AgentPolicy getInstance() { + AgentPolicy p = instance; if (p == null) { throw new IllegalStateException( - "SolrSecurityPolicy has not been initialized. " + "AgentPolicy has not been initialized. " + "Ensure the Solr security agent JAR is on the -javaagent: command-line."); } return p; @@ -183,6 +191,14 @@ public Set trustedFileSystems() { return trustedFileSystems; } + /** + * Host strings exempt from outbound network checks (e.g. {@code "localhost"}, {@code + * "127.0.0.1"}). Populated by {@link SolrAgentEntryPoint} at startup. + */ + public Set trustedHosts() { + return trustedHosts; + } + // --------------------------------------------------------------------------- // Policy checks (convenience helpers called by interceptors) // --------------------------------------------------------------------------- @@ -219,4 +235,25 @@ public boolean isExecApproved(String className) { } return false; } + + /** + * Returns {@code true} if any class in the call chain is approved to call {@code System.exit()} + * or {@code Runtime.halt()}. Mirrors {@code AgentPolicy.isChainThatCanExit()} in the OpenSearch + * reference implementation: any approved class anywhere in the chain grants permission. + * + *

    Class names are matched using {@link String#matches} (full regex), so the approved-caller + * list supports wildcard patterns such as {@code "org\\.apache\\.solr\\..*"}. + * + * @param chain the full set of non-hidden classes in the call stack + */ + public boolean isChainThatCanExit(Collection> chain) { + for (Class cls : chain) { + for (ApprovedCallSite cs : approvedExitCallers) { + if (cs.operation() == ApprovedCallSite.Operation.EXIT && cs.matches(cls.getName())) { + return true; + } + } + } + return false; + } } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ExitInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ExitInterceptor.java deleted file mode 100644 index 540cf2dade7f..000000000000 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ExitInterceptor.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.solr.security.agent; - -import net.bytebuddy.asm.Advice; - -/** - * ByteBuddy {@link Advice} interceptor for {@code System.exit()} and {@code Runtime.halt()}. - * - *

    Checks the top caller class against the {@link SolrSecurityPolicy#approvedExitCallers()} list. - * If the caller is not approved, the violation is logged and (in enforce mode) a {@link - * SecurityException} is thrown, preventing the JVM from terminating. - * - *

    Default approved callers (from the bundled default policy): - * - *

      - *
    • {@code org.apache.solr.cli.SolrCLI} — CLI shutdown commands - *
    • {@code org.apache.solr.servlet.SolrDispatchFilter} — servlet container shutdown hook - *
    - * - *

    Operators may add additional approved callers via {@code agent-security-extra.policy} using a - * codeBase-scoped {@code RuntimePermission "exitVM"} grant. - */ -public final class ExitInterceptor { - - private ExitInterceptor() {} - - /** - * Called before {@code System.exit(int)}. - * - * @param status the exit status code (unused for the policy check) - */ - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onSystemExit(@Advice.Argument(0) int status) { - checkExit("System.exit(" + status + ")"); - } - - /** - * Called before {@code Runtime.halt(int)}. - * - * @param status the halt status code (unused for the policy check) - */ - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onRuntimeHalt(@Advice.Argument(0) int status) { - checkExit("Runtime.halt(" + status + ")"); - } - - // --------------------------------------------------------------------------- - // Core check logic - // --------------------------------------------------------------------------- - - /** - * Checks whether the current top caller is approved to call {@code System.exit()} or {@code - * Runtime.halt()}. Delegates to {@link SecurityViolationLogger} on violation. - * - * @param target a human-readable description of the intercepted call for the violation log - */ - static void checkExit(String target) { - if (!SolrSecurityPolicy.isInitialized()) return; - - SolrSecurityPolicy policy = SolrSecurityPolicy.getInstance(); - String caller = StackInspector.topCallerClassName(); - - if (!policy.isExitApproved(caller)) { - ViolationMetricsReporter.incrementExit(); - SecurityViolationLogger.log( - SecurityViolationLogger.ViolationType.SYSTEM_EXIT, - target, - caller, - policy.enforcementMode()); - if (policy.enforcementMode() == SolrSecurityPolicy.EnforcementMode.ENFORCE) { - throw new SecurityException( - "System.exit() / Runtime.halt() denied by Solr security agent — unapproved caller: " - + caller); - } - } - } -} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileAccessInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileAccessInterceptor.java deleted file mode 100644 index 6dc86037c609..000000000000 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileAccessInterceptor.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.solr.security.agent; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import net.bytebuddy.asm.Advice; - -/** - * ByteBuddy {@link Advice} interceptor for file-system operations. - * - *

    This class is injected into the bootstrap classloader via {@link - * net.bytebuddy.dynamic.loading.ClassInjector.UsingUnsafe} so that it can intercept JDK methods. - * Every public method annotated with {@code Advice.OnMethodEnter} runs before the intercepted - * method body. - * - *

    Enforcement

    - * - *
      - *
    1. Windows UNC paths ({@code \\host\share\...}) are rejected unconditionally regardless of any - * policy rule (FR-003). - *
    2. Symlinks are resolved to their real path via {@code toRealPath()} before the policy check, - * preventing symlink-escape attacks (FR-004). - *
    3. The resolved path is checked against {@link SolrSecurityPolicy#permittedPaths()} for the - * relevant action. - *
    4. Violations are handed to {@link SecurityViolationLogger} and, in enforce mode, result in a - * {@link SecurityException} being thrown. - *
    - * - *

    Trusted filesystems

    - * - * Paths on filesystem schemes listed in {@link SolrSecurityPolicy#trustedFileSystems()} (e.g. - * in-memory filesystems used by tests) are exempt from enforcement. - */ -public final class FileAccessInterceptor { - - private FileAccessInterceptor() {} - - /** - * Called before any NIO file-read operation. Checks the resolved path against the policy. - * - * @param path the {@link Path} argument of the intercepted method - */ - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onFileRead(@Advice.Argument(0) Path path) { - checkPath(path, "read", SecurityViolationLogger.ViolationType.FILE_READ); - } - - /** - * Called before any NIO file-write or create operation. - * - * @param path the {@link Path} argument of the intercepted method - */ - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onFileWrite(@Advice.Argument(0) Path path) { - checkPath(path, "write", SecurityViolationLogger.ViolationType.FILE_WRITE); - } - - /** - * Called before any NIO file-delete operation. - * - * @param path the {@link Path} argument of the intercepted method - */ - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onFileDelete(@Advice.Argument(0) Path path) { - checkPath(path, "delete", SecurityViolationLogger.ViolationType.FILE_DELETE); - } - - // --------------------------------------------------------------------------- - // Core check logic — shared by all three entry points - // --------------------------------------------------------------------------- - - /** - * Performs the UNC check, symlink resolution, and policy lookup for a given path and action. - * Delegates to {@link SecurityViolationLogger} on violation, and (in enforce mode) throws {@link - * SecurityException}. - */ - static void checkPath(Path path, String action, SecurityViolationLogger.ViolationType type) { - if (path == null) return; - - // Check if the policy singleton is available yet; skip if not (very early startup). - if (!SolrSecurityPolicy.isInitialized()) return; - - SolrSecurityPolicy policy = SolrSecurityPolicy.getInstance(); - - // Skip trusted filesystem schemes (e.g. in-memory FS used by tests). - String scheme = path.toUri().getScheme(); - if (scheme != null && policy.trustedFileSystems().contains(scheme)) return; - - String rawPathStr = path.toAbsolutePath().toString(); - - // FR-003: Block Windows UNC paths unconditionally on all platforms. - if (isUncPath(rawPathStr)) { - String caller = StackInspector.topCallerClassName(); - ViolationMetricsReporter.incrementFile(); - SecurityViolationLogger.log( - SecurityViolationLogger.ViolationType.FILE_READ, - rawPathStr, - caller, - policy.enforcementMode(), - "UNC_BLOCKED"); - if (policy.enforcementMode() == SolrSecurityPolicy.EnforcementMode.ENFORCE) { - throw new SecurityException("UNC path access denied by Solr security agent: " + rawPathStr); - } - return; - } - - // FR-004: Resolve symlinks before the policy check. - String resolvedPathStr = resolveRealPath(path, rawPathStr); - - // Check against policy. - if (!policy.isPathPermitted(resolvedPathStr, action)) { - String caller = StackInspector.topCallerClassName(); - ViolationMetricsReporter.incrementFile(); - SecurityViolationLogger.log(type, resolvedPathStr, caller, policy.enforcementMode()); - if (policy.enforcementMode() == SolrSecurityPolicy.EnforcementMode.ENFORCE) { - throw new SecurityException( - "File " + action + " denied by Solr security agent: " + resolvedPathStr); - } - } - } - - /** String-based overload for legacy {@link java.io.File} paths. */ - static void checkPath(String pathStr, String action, SecurityViolationLogger.ViolationType type) { - if (pathStr == null || pathStr.isEmpty()) return; - checkPath(Path.of(pathStr), action, type); - } - - /** - * Returns {@code true} if the given path string is a Windows UNC path ({@code \\host\share\...} - * or the forward-slash equivalent {@code //host/share/...}). This check is platform-independent - * so UNC paths are rejected on Linux and macOS too. - */ - static boolean isUncPath(String pathStr) { - return pathStr.startsWith("\\\\") || pathStr.startsWith("//"); - } - - /** - * Resolves the real (symlink-free) path. Returns the original path string if {@code toRealPath()} - * throws an {@link IOException} (e.g. the file does not yet exist — a pre-create check) or if a - * {@code SecurityException} is thrown (e.g. by the Java SecurityManager in the test environment). - * The unresolved path string is still subject to the policy check. - */ - private static String resolveRealPath(Path path, String fallback) { - try { - return path.toRealPath().toString(); - } catch (IOException | SecurityException e) { - // File may not exist yet (pre-create), or access is restricted; use the normalized path. - return path.normalize().toAbsolutePath().toString(); - } - } - - // --------------------------------------------------------------------------- - // Legacy java.io.File path interception helper - // --------------------------------------------------------------------------- - - /** - * Entry point for {@code java.io.File}-based operations (e.g. {@code FileInputStream}, {@code - * FileOutputStream}). Converts the {@code File} to a {@code Path} and delegates. - */ - @SuppressForbidden( - reason = - "java.io.File is the parameter type of the intercepted legacy JDK method " - + "(e.g. FileInputStream(File)); the Advice method signature must match.") - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onFileIo(@Advice.Argument(0) File file) { - if (file == null) return; - checkPath(file.toPath(), "read", SecurityViolationLogger.ViolationType.FILE_READ); - } -} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java new file mode 100644 index 000000000000..cfa2e82c9df3 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java @@ -0,0 +1,269 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.lang.reflect.Method; +import java.nio.file.LinkOption; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.file.spi.FileSystemProvider; +import java.util.Collection; +import java.util.Locale; +import java.util.Set; +import net.bytebuddy.asm.Advice; + +/** + * ByteBuddy {@link Advice} interceptor for file-system operations. + * + *

    This file was derived from the OpenSearch project and modified. See {@code NOTICE.txt} for + * attribution. + */ +public class FileInterceptor { + + /** FileInterceptor */ + public FileInterceptor() {} + + /** + * Intercepts file operations. + * + * @param args arguments + * @param method method + * @throws Exception exceptions + */ + @Advice.OnMethodEnter + public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin Method method) + throws Exception { + if (!AgentPolicy.isInitialized()) return; + final AgentPolicy policy = AgentPolicy.getInstance(); + + FileSystemProvider provider = null; + String filePath = null; + if (args.length > 0 && args[0] instanceof String pathStr) { + filePath = Path.of(pathStr).toAbsolutePath().toString(); + } else if (args.length > 0 && args[0] instanceof Path path) { + filePath = path.toAbsolutePath().toString(); + provider = path.getFileSystem().provider(); + } + + if (filePath == null) { + return; // No valid file path found + } + + if (provider != null && policy.trustedFileSystems().contains(provider.getScheme())) { + return; + } + + final StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); + final String caller = walker.getCallerClass().getName(); + + final String name = method.getName(); + boolean isMutating = name.equals("move") || name.equals("write") || name.startsWith("create"); + final boolean isDelete = isMutating == false ? name.startsWith("delete") : false; + + // This is Windows implementation of UNIX Domain Sockets (close) + boolean isUnixSocketCaller = false; + if (isDelete == true) { + final Collection> chain = walker.walk(StackCallerClassChainExtractor.INSTANCE); + + if (walker + .getCallerClass() + .getName() + .equalsIgnoreCase("sun.nio.ch.PipeImpl$Initializer$LoopbackConnector") + == true) { + isUnixSocketCaller = true; + } else { + for (final Class cls : chain) { + if (cls.getName().equalsIgnoreCase("sun.nio.ch.PipeImpl$Initializer$LoopbackConnector")) { + isUnixSocketCaller = true; + break; + } + } + } + } + + if (isDelete == true && isUnixSocketCaller == true) { + // Unix domain socket cleanup — local IPC, always allow + return; + } else { + String targetFilePath = null; + if (isMutating == false && isDelete == false) { + if (name.equals("newByteChannel") == true || name.equals("open") == true) { + if (args.length > 1 && args instanceof Object) { + if (args instanceof OpenOption[] opts) { + for (final OpenOption opt : opts) { + if (opt != StandardOpenOption.READ) { + isMutating = true; + break; + } + } + } else if (args[1] instanceof Set opts) { + @SuppressWarnings("unchecked") + final Set options = (Set) args[1]; + for (final OpenOption opt : options) { + if (opt != StandardOpenOption.READ) { + isMutating = true; + break; + } + } + } else if (args[1] instanceof Object[] opts) { + for (final Object opt : opts) { + if (opt != StandardOpenOption.READ) { + isMutating = true; + break; + } + } + } else { + throw new SecurityException( + "Unsupported argument type: " + args[1].getClass().getName()); + } + } + } else if (name.equals("copy") == true) { + if (args.length > 1 && args[1] instanceof String pathStr) { + targetFilePath = Path.of(pathStr).toAbsolutePath().toString(); + } else if (args.length > 1 && args[1] instanceof Path path) { + targetFilePath = path.toAbsolutePath().toString(); + } + } + } + + // Handle FileChannel.open() and newByteChannel() — check read/write permissions + if (method.getName().equals("open") || method.getName().equals("newByteChannel")) { + final String action = isMutating ? "write" : "read"; + if (!policy.isPathPermitted(filePath, action)) { + ViolationMetricsReporter.incrementFile(); + SecurityViolationLogger.log( + isMutating + ? SecurityViolationLogger.ViolationType.FILE_WRITE + : SecurityViolationLogger.ViolationType.FILE_READ, + filePath, + caller, + policy.enforcementMode()); + if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException( + "Denied " + + (isMutating ? "OPEN (read/write)" : "OPEN (read)") + + " access to file: " + + filePath); + } + } + } + + // Handle Files.copy() — check source read and target write permissions + if (method.getName().equals("copy")) { + if (!policy.isPathPermitted(filePath, "read")) { + ViolationMetricsReporter.incrementFile(); + SecurityViolationLogger.log( + SecurityViolationLogger.ViolationType.FILE_READ, + filePath, + caller, + policy.enforcementMode()); + if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException("Denied COPY (read) access to file: " + filePath); + } + } + if (targetFilePath != null && !policy.isPathPermitted(targetFilePath, "write")) { + ViolationMetricsReporter.incrementFile(); + SecurityViolationLogger.log( + SecurityViolationLogger.ViolationType.FILE_WRITE, + targetFilePath, + caller, + policy.enforcementMode()); + if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException("Denied COPY (write) access to file: " + targetFilePath); + } + } + } + + // File mutating operations + if (isMutating && !policy.isPathPermitted(filePath, "write")) { + ViolationMetricsReporter.incrementFile(); + SecurityViolationLogger.log( + SecurityViolationLogger.ViolationType.FILE_WRITE, + filePath, + caller, + policy.enforcementMode()); + if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException("Denied WRITE access to file: " + filePath); + } + } + + // File deletion operations + if (isDelete && !policy.isPathPermitted(filePath, "delete")) { + ViolationMetricsReporter.incrementFile(); + SecurityViolationLogger.log( + SecurityViolationLogger.ViolationType.FILE_DELETE, + filePath, + caller, + policy.enforcementMode()); + if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException("Denied DELETE access to file: " + filePath); + } + } + } + } + + // --------------------------------------------------------------------------- + // Static helpers (used by advice and by tests) + // --------------------------------------------------------------------------- + + /** + * Resolves the real path of {@code path}, following symlinks. Falls back to {@code + * normalize().toAbsolutePath()} if the file does not exist or if a security manager blocks the + * call. + */ + public static String topCallerClassName() { + try { + return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .getCallerClass() + .getName(); + } catch (Exception e) { + return ""; + } + } + + public static String resolveRealPath(Path path) { + try { + return path.toRealPath(new LinkOption[0]).toString(); + } catch (Exception e) { + return path.normalize().toAbsolutePath().toString(); + } + } + + /** + * Checks whether {@code path} may be accessed with {@code action} under the active policy. + * Increments the file violation counter and logs on violation; throws {@link SecurityException} + * in enforce mode. + * + *

    Used by tests to exercise the file-access check without ByteBuddy instrumentation. + */ + public static void checkPath( + Path path, String action, SecurityViolationLogger.ViolationType violationType) { + if (!AgentPolicy.isInitialized()) return; + AgentPolicy policy = AgentPolicy.getInstance(); + String resolvedPath = resolveRealPath(path); + String caller = topCallerClassName(); + if (!policy.isPathPermitted(resolvedPath, action)) { + ViolationMetricsReporter.incrementFile(); + SecurityViolationLogger.log(violationType, resolvedPath, caller, policy.enforcementMode()); + if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException( + "Denied " + action.toUpperCase(Locale.ROOT) + " access to: " + resolvedPath); + } + } + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/NetworkAccessInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/NetworkAccessInterceptor.java deleted file mode 100644 index c6451777b5e0..000000000000 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/NetworkAccessInterceptor.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.solr.security.agent; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import net.bytebuddy.asm.Advice; - -/** - * ByteBuddy {@link Advice} interceptor for outbound network connections. - * - *

    Intercepts {@code SocketChannel.connect(SocketAddress)} and {@code - * Socket.connect(SocketAddress)} to enforce the {@link SolrSecurityPolicy#permittedEndpoints()} - * list. Loopback addresses are unconditionally allowed by this interceptor regardless of policy - * entries, as a safety net against policy misconfiguration. - * - *

    Port-wildcard entries in the default policy (e.g. {@code *:8983}) are matched by comparing - * only the port number when the host portion of the entry is {@code *}. - */ -public final class NetworkAccessInterceptor { - - private NetworkAccessInterceptor() {} - - /** - * Called before {@code SocketChannel.connect()} or {@code Socket.connect()}. - * - * @param address the remote address to connect to - */ - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onConnect(@Advice.Argument(0) SocketAddress address) { - if (!(address instanceof InetSocketAddress)) return; - checkConnect((InetSocketAddress) address); - } - - // --------------------------------------------------------------------------- - // Core check logic - // --------------------------------------------------------------------------- - - /** - * Checks the given resolved remote address against the active policy's permitted endpoint list. - * Loopback and unresolved addresses are allowed unconditionally. - */ - static void checkConnect(InetSocketAddress address) { - if (!SolrSecurityPolicy.isInitialized()) return; - if (address.isUnresolved()) return; - - InetAddress inetAddress = address.getAddress(); - if (inetAddress != null && inetAddress.isLoopbackAddress()) return; - - SolrSecurityPolicy policy = SolrSecurityPolicy.getInstance(); - int port = address.getPort(); - String host = inetAddress != null ? inetAddress.getHostAddress() : address.getHostName(); - - if (!isEndpointPermitted(policy, host, port)) { - String target = host + ":" + port; - String caller = StackInspector.topCallerClassName(); - ViolationMetricsReporter.incrementNetwork(); - SecurityViolationLogger.log( - SecurityViolationLogger.ViolationType.NETWORK_CONNECT, - target, - caller, - policy.enforcementMode()); - if (policy.enforcementMode() == SolrSecurityPolicy.EnforcementMode.ENFORCE) { - throw new SecurityException( - "Outbound network connection denied by Solr security agent: " + target); - } - } - } - - /** - * Returns {@code true} if at least one permitted endpoint entry in the policy covers the given - * host and port. Matching rules: - * - *

      - *
    • Entry {@code *:port} — matches any host on that exact port - *
    • Entry {@code host:port} — matches exact host and port - *
    • Entry {@code host:low-high} — matches the host with a port in the inclusive range - *
    • Entry {@code *} (no colon) — matches everything (broad wildcard) - *
    - * - *

    CodBase-scoped entries ({@link PermittedEndpoint#codeBase()} non-null) are skipped in this - * path-based check; they are handled by the JVM's own permission system for code loaded from the - * specified location. - */ - static boolean isEndpointPermitted(SolrSecurityPolicy policy, String host, int port) { - for (PermittedEndpoint entry : policy.permittedEndpoints()) { - // Skip codeBase-scoped entries; they are handled separately at the module level. - if (entry.codeBase() != null) continue; - - String hostPort = entry.hostPort(); - if (matchesEndpoint(hostPort, host, port)) return true; - } - return false; - } - - private static boolean matchesEndpoint(String hostPortEntry, String host, int port) { - if ("*".equals(hostPortEntry)) return true; // broad wildcard - - int colonIdx = hostPortEntry.lastIndexOf(':'); - if (colonIdx < 0) { - // host-only entry — matches any port on that host - return matchesHost(hostPortEntry, host); - } - - String entryHost = hostPortEntry.substring(0, colonIdx); - String entryPort = hostPortEntry.substring(colonIdx + 1); - - if (!matchesHost(entryHost, host)) return false; - - return matchesPort(entryPort, port); - } - - private static boolean matchesHost(String entryHost, String actualHost) { - if ("*".equals(entryHost)) return true; - return entryHost.equalsIgnoreCase(actualHost); - } - - private static boolean matchesPort(String entryPort, int actualPort) { - if ("*".equals(entryPort)) return true; - int dashIdx = entryPort.indexOf('-'); - if (dashIdx < 0) { - try { - return Integer.parseInt(entryPort.trim()) == actualPort; - } catch (NumberFormatException e) { - return false; - } - } - // Range: low-high - try { - int low = Integer.parseInt(entryPort.substring(0, dashIdx).trim()); - int high = Integer.parseInt(entryPort.substring(dashIdx + 1).trim()); - return actualPort >= low && actualPort <= high; - } catch (NumberFormatException e) { - return false; - } - } -} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java new file mode 100644 index 000000000000..fd8c0a450a26 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.io.IOException; +import java.io.Reader; +import java.io.StreamTokenizer; +import java.util.ArrayList; +import java.util.List; + +/** + * Tokenizer-based parser for JDK-style {@code .policy} files. + * + *

    Uses {@link StreamTokenizer} which natively handles {@code //} line comments, {@code /* *\/} + * block comments, and quoted strings — no regex involved. + * + *

    This file was derived from the OpenSearch project and modified. See {@code NOTICE.txt} for + * attribution. + */ +class PolicyFileParser { + + private PolicyFileParser() {} + + record GrantEntry(String codeBase, List permissions) {} + + record PermEntry(String permission, String name, String action) {} + + static class ParsingException extends Exception { + ParsingException(String message) { + super(message); + } + + ParsingException(int line, String expected, String found) { + super("line " + line + ": expected [" + expected + "], found [" + found + "]"); + } + } + + static List read(Reader policy) throws ParsingException, IOException { + List entries = new ArrayList<>(); + PolicyTokenStream ts = new PolicyTokenStream(policy); + while (!ts.isEOF()) { + if (peek(ts, "grant")) { + entries.add(parseGrantEntry(ts)); + } else { + // skip unexpected top-level token + ts.consume(); + } + } + return entries; + } + + private static GrantEntry parseGrantEntry(PolicyTokenStream ts) + throws ParsingException, IOException { + String codeBase = null; + List perms = new ArrayList<>(); + + poll(ts, "grant"); + + while (!peek(ts, "{")) { + if (pollOnMatch(ts, "codeBase")) { + String raw = poll(ts, ts.peek().text()); + codeBase = expand(ts, raw); + } else { + // Skip unknown grant modifiers + ts.consume(); + } + } + + poll(ts, "{"); + + while (!peek(ts, "}")) { + if (ts.isEOF()) break; + if (peek(ts, "permission")) { + perms.add(parsePermEntry(ts)); + pollOnMatch(ts, ";"); + } else { + ts.consume(); // skip unexpected token inside grant block + } + } + + poll(ts, "}"); + pollOnMatch(ts, ";"); + + return new GrantEntry(codeBase, List.copyOf(perms)); + } + + private static PermEntry parsePermEntry(PolicyTokenStream ts) + throws ParsingException, IOException { + poll(ts, "permission"); + String permClass = poll(ts, ts.peek().text()); + + String name = null; + if (isQuoted(ts.peek())) { + name = expand(ts, poll(ts, ts.peek().text())); + } + + String action = null; + pollOnMatch(ts, ","); + if (isQuoted(ts.peek())) { + action = expand(ts, poll(ts, ts.peek().text())); + } + + return new PermEntry(permClass, name, action); + } + + private static String expand(PolicyTokenStream ts, String raw) throws ParsingException { + try { + return PolicyPropertyExpander.expand(raw); + } catch (PolicyPropertyExpander.ExpandException e) { + try { + throw new ParsingException(ts.line(), e.getMessage(), raw); + } catch (IOException ioe) { + throw new ParsingException(e.getMessage()); + } + } + } + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + private static boolean peek(PolicyTokenStream ts, String expected) throws IOException { + return expected.equalsIgnoreCase(ts.peek().text()); + } + + private static boolean pollOnMatch(PolicyTokenStream ts, String expected) + throws ParsingException, IOException { + if (peek(ts, expected)) { + poll(ts, expected); + return true; + } + return false; + } + + private static String poll(PolicyTokenStream ts, String expected) + throws ParsingException, IOException { + PolicyToken token = ts.consume(); + boolean isKeywordOrSymbol = + expected.equalsIgnoreCase("grant") + || expected.equalsIgnoreCase("codeBase") + || expected.equalsIgnoreCase("permission") + || expected.equals("{") + || expected.equals("}") + || expected.equals(";") + || expected.equals(","); + if (isKeywordOrSymbol && !expected.equalsIgnoreCase(token.text())) { + throw new ParsingException(token.line(), expected, token.text()); + } + return token.text(); + } + + private static boolean isQuoted(PolicyToken token) { + return token.type() == '"' || token.type() == '\''; + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java index 4933d8433bbb..b458d9f36f2f 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java @@ -16,7 +16,6 @@ */ package org.apache.solr.security.agent; -import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.nio.charset.StandardCharsets; @@ -24,30 +23,18 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.Set; /** * Reads and parses JDK-style {@code .policy} files, performing Solr-specific variable substitution, - * and produces a {@link SolrSecurityPolicy} ready for enforcement. + * and produces a {@link AgentPolicy} ready for enforcement. * *

    Variable substitution

    * - * The following variables are expanded in permission targets and {@code codeBase} URLs before - * parsing: - * - *
      - *
    • {@code ${solr.home}} — Solr home directory - *
    • {@code ${solr.data.dir}} — Solr data directory - *
    • {@code ${solr.log.dir}} — Solr log directory - *
    • {@code ${solr.install.dir}} — Solr installation root (parent of {@code server/}) - *
    • {@code ${java.io.tmpdir}} — JVM temporary directory - *
    • {@code ${java.home}} — JDK installation directory - *
    • {@code ${user.home}} — OS user home directory - *
    • {@code ${solr.port}} — Solr HTTP port (from system property {@code solr.port}) - *
    • {@code ${solr.zk.port}} — ZooKeeper port; defaults to {@code solr.port + 1000} when not - * explicitly configured - *
    + * {@code ${property}} placeholders in permission targets and {@code codeBase} URLs are expanded + * per-token by {@link PolicyPropertyExpander} using system properties. Any unresolved placeholder + * causes startup to fail immediately (fail-fast). The only built-in default is {@code + * ${solr.zk.port}}, which falls back to {@code solr.port + 1000} when not explicitly set. * *

    Two-file merge

    * @@ -68,28 +55,14 @@ public enum PolicySource { OPERATOR } - // Variables expanded in path and codeBase expressions before parsing. - private static final String[] SYSTEM_VARS = { - "solr.home", - "solr.data.dir", - "solr.log.dir", - "solr.install.dir", - "java.io.tmpdir", - "java.home", - "user.home", - }; - - // Pattern to match a variable reference such as ${solr.home} - private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\{([^}]+)\\}"); - /** * Loads and merges the default policy file and the optional operator extension file. * * @param defaultPolicyPath absolute path to the default {@code agent-security.policy} file - * @return a fully initialized {@link SolrSecurityPolicy} + * @return a fully initialized {@link AgentPolicy} * @throws IllegalStateException if the default policy file is absent or cannot be parsed */ - public SolrSecurityPolicy load(Path defaultPolicyPath) { + public AgentPolicy load(Path defaultPolicyPath) { if (!Files.exists(defaultPolicyPath)) { throw new IllegalStateException( "Security agent default policy not found: " @@ -132,7 +105,8 @@ public SolrSecurityPolicy load(Path defaultPolicyPath) { * available. */ static Path resolveExtraPolicyPath() { - String explicitPath = System.getProperty("solr.security.agent.extra.policy"); + String explicitPath = + PolicyPropertyExpander.getPropertyOrEnv("solr.security.agent.extra.policy"); if (explicitPath != null && !explicitPath.isBlank()) { return Path.of(explicitPath); } @@ -144,59 +118,13 @@ static Path resolveExtraPolicyPath() { } /** - * Parses a policy file, substituting variables, and appends the resulting {@link GrantBlock} - * entries — tagged with the given {@code source} — to {@code out}. + * Parses a policy file and appends the resulting {@link GrantBlock} entries — tagged with the + * given {@code source} — to {@code out}. Variable substitution is performed per-token by {@link + * PolicyPropertyExpander}; any unresolved {@code ${variable}} causes an {@link + * IllegalStateException}. */ static void parsePolicy(String content, PolicySource source, List out) { - String expanded = substituteVariables(content); - parsePolicyBlocks(expanded, source, out); - } - - /** - * Expands all {@code ${variable}} references in {@code text}. Unknown variables are left as-is. - * {@code ${solr.port}} and {@code ${solr.zk.port}} receive special handling so that the ZK port - * defaults to {@code solr.port + 1000} when not explicitly set. - */ - static String substituteVariables(String text) { - // Resolve solr.port once so we can derive the default ZK port. - String solrPortStr = System.getProperty("solr.port", "8983"); - int solrPort; - try { - solrPort = Integer.parseInt(solrPortStr.trim()); - } catch (NumberFormatException e) { - solrPort = 8983; - } - String zkPortStr = System.getProperty("solr.zk.port"); - String zkPort = - (zkPortStr != null && !zkPortStr.isBlank()) - ? zkPortStr.trim() - : String.valueOf(solrPort + 1000); - - StringBuffer sb = new StringBuffer(); - Matcher m = VAR_PATTERN.matcher(text); - while (m.find()) { - String varName = m.group(1); - String replacement; - if ("solr.port".equals(varName)) { - replacement = solrPortStr.trim(); - } else if ("solr.zk.port".equals(varName)) { - replacement = zkPort; - } else { - replacement = resolveSystemVar(varName); - } - m.appendReplacement( - sb, Matcher.quoteReplacement(replacement != null ? replacement : m.group(0))); - } - m.appendTail(sb); - return sb.toString(); - } - - private static String resolveSystemVar(String varName) { - // Try system property first, then environment-style lookup. - String val = System.getProperty(varName); - if (val != null) return val; - // For well-known vars, also try without dots. - return null; + parsePolicyBlocks(content, source, out); } /** @@ -209,97 +137,62 @@ private static String resolveSystemVar(String varName) { *
  • {@code java.lang.RuntimePermission "exitVM"} → {@link ApprovedCallSite} EXIT *
  • {@code java.lang.RuntimePermission "exec"} → {@link ApprovedCallSite} EXEC * + * + *

    Parsing uses {@link PolicyFileParser} (backed by {@link java.io.StreamTokenizer}) which + * natively handles {@code //} and {@code /* *\/} comments and quoted strings — no regex. */ static void parsePolicyBlocks(String text, PolicySource source, List out) { - // Strip single-line comments - String noComments = stripComments(text); - - // Match: grant [codeBase "url"] { ... }; - Pattern grantPattern = - Pattern.compile( - "grant\\s*(?:codeBase\\s*\"([^\"]*?)\")?\\s*\\{([^}]*)\\}\\s*;", - Pattern.CASE_INSENSITIVE | Pattern.DOTALL); - - Matcher grantMatcher = grantPattern.matcher(noComments); - while (grantMatcher.find()) { - String codeBase = grantMatcher.group(1); // null for global grants - String body = grantMatcher.group(2); - GrantBlock block = new GrantBlock(codeBase, source); - parsePermissions(body, block); - out.add(block); + List grantEntries; + try { + grantEntries = PolicyFileParser.read(new StringReader(text)); + } catch (PolicyFileParser.ParsingException | IOException e) { + throw new IllegalStateException("Failed to parse security policy: " + e.getMessage(), e); } - } - - private static String stripComments(String text) { - // Remove // line comments; leave /* */ block comments as-is (not used in standard policy files) - StringBuilder sb = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new StringReader(text))) { - String line; - while ((line = reader.readLine()) != null) { - int commentIdx = line.indexOf("//"); - if (commentIdx >= 0) { - line = line.substring(0, commentIdx); - } - sb.append(line).append('\n'); + for (PolicyFileParser.GrantEntry ge : grantEntries) { + GrantBlock block = new GrantBlock(ge.codeBase(), source); + for (PolicyFileParser.PermEntry pe : ge.permissions()) { + addPermission(pe, block); } - } catch (IOException e) { - // StringReader never throws - throw new AssertionError(e); + out.add(block); } - return sb.toString(); } - /** - * Parses individual {@code permission} lines inside a grant block body and adds recognised - * permissions to the block. - */ - static void parsePermissions(String body, GrantBlock block) { - // permission ["target"] [, "actions"]; - Pattern permPattern = - Pattern.compile( - "permission\\s+(\\S+)\\s*(?:\"([^\"]*?)\")?\\s*(?:,\\s*\"([^\"]*?)\")?\\s*;", - Pattern.CASE_INSENSITIVE); - - Matcher m = permPattern.matcher(body); - while (m.find()) { - String permClass = m.group(1); - String target = m.group(2); // may be null - String actions = m.group(3); // may be null - - switch (permClass) { - case "java.io.FilePermission": - if (target != null) { - block.filePaths.add( - new RawFilePermission(target, actions != null ? actions : "read", block.source)); - } - break; - case "java.net.SocketPermission": - if (target != null) { - block.socketPerms.add( - new RawSocketPermission( - target, - actions != null ? actions : "connect,resolve", - block.codeBase, - block.source)); - } - break; - case "java.lang.RuntimePermission": - if ("exitVM".equals(target) || (target != null && target.startsWith("exitVM."))) { - block.runtimePerms.add( - new RawRuntimePermission("exitVM", block.codeBase, block.source)); - } else if ("exec".equals(target)) { - block.runtimePerms.add(new RawRuntimePermission("exec", block.codeBase, block.source)); - } - break; - default: - // Unrecognised permission types are ignored (e.g. PropertyPermission in legacy policy) - break; - } + private static void addPermission(PolicyFileParser.PermEntry pe, GrantBlock block) { + String permClass = pe.permission(); + String target = pe.name(); + String actions = pe.action(); + switch (permClass) { + case "java.io.FilePermission": + if (target != null) { + block.filePaths.add( + new RawFilePermission(target, actions != null ? actions : "read", block.source)); + } + break; + case "java.net.SocketPermission": + if (target != null) { + block.socketPerms.add( + new RawSocketPermission( + target, + actions != null ? actions : "connect,resolve", + block.codeBase, + block.source)); + } + break; + case "java.lang.RuntimePermission": + if ("exitVM".equals(target) || (target != null && target.startsWith("exitVM."))) { + block.runtimePerms.add(new RawRuntimePermission("exitVM", block.codeBase, block.source)); + } else if ("exec".equals(target)) { + block.runtimePerms.add(new RawRuntimePermission("exec", block.codeBase, block.source)); + } + break; + default: + // Unrecognised permission types are ignored (e.g. PropertyPermission in legacy policy) + break; } } - /** Converts raw parsed grant blocks into an immutable {@link SolrSecurityPolicy}. */ - private SolrSecurityPolicy buildPolicy(List grants) { + /** Converts raw parsed grant blocks into an immutable {@link AgentPolicy}. */ + private AgentPolicy buildPolicy(List grants) { List paths = new ArrayList<>(); List endpoints = new ArrayList<>(); List exitCallers = new ArrayList<>(); @@ -328,15 +221,18 @@ private SolrSecurityPolicy buildPolicy(List grants) { } } - // Read enforcement mode from system property (not from EnvUtils — agent has no dep on - // solr:core) - String modeStr = System.getProperty("solr.security.agent.mode", "warn"); - SolrSecurityPolicy.EnforcementMode mode = + // Read enforcement mode from sysprop or env var (agent has no dep on solr:core/EnvUtils) + String modeStr = PolicyPropertyExpander.getPropertyOrEnv("solr.security.agent.mode"); + if (modeStr == null) modeStr = "warn"; + AgentPolicy.EnforcementMode mode = "enforce".equalsIgnoreCase(modeStr.trim()) - ? SolrSecurityPolicy.EnforcementMode.ENFORCE - : SolrSecurityPolicy.EnforcementMode.WARN; + ? AgentPolicy.EnforcementMode.ENFORCE + : AgentPolicy.EnforcementMode.WARN; - return new SolrSecurityPolicy(paths, endpoints, exitCallers, execCallers, mode); + Set trustedHosts = + Set.of("localhost", "127.0.0.1", "0:0:0:0:0:0:0:1", "::1", "0.0.0.0"); + return new AgentPolicy( + paths, endpoints, exitCallers, execCallers, mode, Set.of(), trustedHosts); } // --------------------------------------------------------------------------- diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyPropertyExpander.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyPropertyExpander.java new file mode 100644 index 000000000000..7862f49662fd --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyPropertyExpander.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Expands {@code ${property}} placeholders in policy file token values using system properties. + * + *

    Throws {@link ExpandException} for any unresolved placeholder — fail-fast behaviour so that a + * misconfigured policy (missing system property) is caught at startup rather than silently + * resulting in an overly-permissive or broken policy. + * + *

    Special cases: + * + *

      + *
    • {@code ${solr.zk.port}} defaults to {@code solr.port.listen + 1000} when absent, mirroring + * the ZooKeeper port convention used throughout Solr startup scripts. + *
    • {@code ${solr.data.home}} defaults to the value of {@code solr.solr.home} when not + * explicitly configured — matching Solr's default data layout. + *
    • {@code ${{literal}}} is left verbatim (escape hatch for values that must contain {@code + * ${...}}). + *
    + * + *

    This file was derived from the OpenSearch project and modified. See {@code NOTICE.txt} for + * attribution. + */ +class PolicyPropertyExpander { + + private PolicyPropertyExpander() {} + + /** Thrown when a {@code ${property}} placeholder cannot be resolved. */ + static class ExpandException extends Exception { + ExpandException(String message) { + super(message); + } + } + + // Matches ${{escaped}} (literal pass-through) or ${normal} (property lookup) + private static final Pattern PLACEHOLDER_PATTERN = + Pattern.compile("\\$\\{\\{(?.*?)}}|\\$\\{(?.*?)}"); + + /** + * Non-standard env var names for sysprops that do not follow the SOLR_FOO_BAR convention. Mirrors + * the custom entries in {@code EnvToSyspropMappings.properties} in SolrJ. + */ + private static final Map CUSTOM_ENV_NAMES = + Map.of( + "solr.solr.home", "SOLR_HOME", + "solr.install.dir", "SOLR_TIP"); + + /** + * Returns the value for {@code sysprop} by checking, in order: + * + *

      + *
    1. {@code System.getProperty(sysprop)} + *
    2. The env var derived from the sysprop name (standard {@code SOLR_FOO_BAR} convention, with + * custom overrides for non-standard names) + *
    + * + * Returns {@code null} if neither source has a value. + */ + static String getPropertyOrEnv(String sysprop) { + String val = System.getProperty(sysprop); + if (val != null && !val.isBlank()) return val.trim(); + String envVar = + CUSTOM_ENV_NAMES.getOrDefault(sysprop, sysprop.toUpperCase(Locale.ROOT).replace('.', '_')); + val = System.getenv(envVar); + return (val != null && !val.isBlank()) ? val.trim() : null; + } + + /** + * Expands all {@code ${property}} placeholders in {@code value}. Returns {@code null} if {@code + * value} is {@code null}. + * + * @throws ExpandException if a placeholder references a system property that is not set + */ + static String expand(String value) throws ExpandException { + if (value == null || !value.contains("${")) { + return value; + } + Matcher matcher = PLACEHOLDER_PATTERN.matcher(value); + StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + String replacement = handleMatch(matcher); + matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(sb); + return sb.toString(); + } + + private static String handleMatch(Matcher match) throws ExpandException { + String escaped = match.group("escaped"); + if (escaped != null) { + // ${{literal}} — return verbatim + return "${{" + escaped + "}}"; + } + return expandPlaceholder(match.group("normal")); + } + + private static String expandPlaceholder(String placeholder) throws ExpandException { + // solr.zk.port is optional — default to solr.port.listen + 1000 + if ("solr.zk.port".equals(placeholder)) { + String val = getPropertyOrEnv("solr.zk.port"); + if (val != null) return val; + String solrPort = getPropertyOrEnv("solr.port.listen"); + if (solrPort == null) solrPort = "8983"; + try { + return String.valueOf(Integer.parseInt(solrPort.trim()) + 1000); + } catch (NumberFormatException e) { + return "9983"; + } + } + // solr.data.home is optional — when not configured, Solr stores data under solr.solr.home + if ("solr.data.home".equals(placeholder)) { + String val = getPropertyOrEnv("solr.data.home"); + if (val != null) return val; + val = getPropertyOrEnv("solr.solr.home"); + if (val != null) return val; + throw new ExpandException( + "Unresolved policy variable: ${solr.data.home} (and ${solr.solr.home} is also unset)"); + } + String value = getPropertyOrEnv(placeholder); + if (value == null) { + throw new ExpandException("Unresolved policy variable: ${" + placeholder + "}"); + } + return value; + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyToken.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyToken.java new file mode 100644 index 000000000000..b7a97d84be37 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyToken.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +record PolicyToken(int type, String text, int line) {} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyTokenStream.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyTokenStream.java new file mode 100644 index 000000000000..9fa790cee24f --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyTokenStream.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.io.IOException; +import java.io.Reader; +import java.io.StreamTokenizer; +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * Token stream backed by a {@link StreamTokenizer} for policy file parsing. + * + *

    This file was derived from the OpenSearch project and modified. See {@code NOTICE.txt} for + * attribution. + */ +class PolicyTokenStream { + private final StreamTokenizer tokenizer; + private final Deque buffer = new ArrayDeque<>(); + + PolicyTokenStream(Reader reader) { + StreamTokenizer st = new StreamTokenizer(reader); + st.resetSyntax(); + st.wordChars('a', 'z'); + st.wordChars('A', 'Z'); + st.wordChars('.', '.'); + st.wordChars('0', '9'); + st.wordChars('_', '_'); + st.wordChars('$', '$'); + st.wordChars(128 + 32, 255); + st.whitespaceChars(0, ' '); + st.commentChar('/'); + st.quoteChar('\''); + st.quoteChar('"'); + st.lowerCaseMode(false); + st.ordinaryChar('/'); + st.slashSlashComments(true); + st.slashStarComments(true); + this.tokenizer = st; + } + + PolicyToken peek() throws IOException { + if (buffer.isEmpty()) { + buffer.push(nextToken()); + } + return buffer.peek(); + } + + PolicyToken consume() throws IOException { + return buffer.isEmpty() ? nextToken() : buffer.pop(); + } + + boolean isEOF() throws IOException { + return peek().type() == StreamTokenizer.TT_EOF; + } + + int line() throws IOException { + return peek().line(); + } + + private PolicyToken nextToken() throws IOException { + int type = tokenizer.nextToken(); + String text = + switch (type) { + case StreamTokenizer.TT_WORD, '"', '\'' -> tokenizer.sval; + case StreamTokenizer.TT_EOF -> ""; + default -> Character.toString((char) type); + }; + return new PolicyToken(type, text, tokenizer.lineno()); + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java index 6ce93194ab5e..548c41ee06a6 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java @@ -16,18 +16,19 @@ */ package org.apache.solr.security.agent; +import java.lang.reflect.Method; import net.bytebuddy.asm.Advice; /** * ByteBuddy {@link Advice} interceptor for child process spawning. * *

    Intercepts {@code ProcessBuilder.start()} and {@code Runtime.exec()} to enforce the {@link - * SolrSecurityPolicy#approvedExecCallers()} list. By default, no call sites are approved in the - * production policy (the list is empty), so all process-spawning attempts will be flagged unless an - * operator explicitly adds an entry to {@code agent-security-extra.policy}. + * AgentPolicy#approvedExecCallers()} list. By default, no call sites are approved in the production + * policy (the list is empty), so all process-spawning attempts will be flagged unless an operator + * explicitly adds an entry to {@code agent-security-extra.policy}. * - *

    This interceptor is not present in the OpenSearch {@code agent-sm} module; it is a Solr - * addition to cover the {@code ProcessBuilder} usage sites in Solr core (FR-007). + *

    This is a Solr-specific interceptor to cover the {@code ProcessBuilder} usage sites in Solr + * core (FR-007). * *

    Known legitimate process-spawning call sites in Solr (kept out of the default production * policy because they use {@code ProcessHandle}, not {@code ProcessBuilder}): @@ -40,24 +41,30 @@ public final class ProcessExecInterceptor { private ProcessExecInterceptor() {} - /** Called before {@code ProcessBuilder.start()}. */ - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onProcessBuilderStart() { - checkExec("ProcessBuilder.start()"); - } - /** - * Called before {@code Runtime.exec(String[])}. + * Single entry point for both {@code ProcessBuilder.start()} and {@code Runtime.exec()}. + * + *

    ByteBuddy requires exactly one {@code @OnMethodEnter} method per advice class. This method + * is registered on both {@code ProcessBuilder} and {@code Runtime} via separate {@code + * AgentBuilder} transform chains in {@link SolrAgentEntryPoint}. * - * @param command the command array (first element used for the violation log target) + * @param args all arguments of the intercepted method + * @param method the intercepted method (used to identify the call site in the violation log) */ - @Advice.OnMethodEnter(suppress = Throwable.class) - public static void onRuntimeExec(@Advice.Argument(0) String[] command) { - String target = - command != null && command.length > 0 - ? "Runtime.exec(" + command[0] + ")" - : "Runtime.exec()"; - checkExec(target); + @Advice.OnMethodEnter(suppress = java.io.IOException.class) + public static void onExec(@Advice.AllArguments Object[] args, @Advice.Origin Method method) { + checkExec(deriveTarget(method.getName(), args)); + } + + public static String deriveTarget(String methodName, Object[] args) { + if (args == null || args.length == 0) return methodName + "()"; + Object arg0 = args[0]; + if (arg0 instanceof String[]) { + String[] cmd = (String[]) arg0; + return methodName + "(" + (cmd.length > 0 ? cmd[0] : "") + ")"; + } + if (arg0 instanceof String) return methodName + "(" + arg0 + ")"; + return methodName + "()"; } // --------------------------------------------------------------------------- @@ -70,11 +77,14 @@ public static void onRuntimeExec(@Advice.Argument(0) String[] command) { * * @param target a human-readable description of the intercepted call for the violation log */ - static void checkExec(String target) { - if (!SolrSecurityPolicy.isInitialized()) return; + public static void checkExec(String target) { + if (!AgentPolicy.isInitialized()) return; - SolrSecurityPolicy policy = SolrSecurityPolicy.getInstance(); - String caller = StackInspector.topCallerClassName(); + AgentPolicy policy = AgentPolicy.getInstance(); + String caller = + StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .getCallerClass() + .getName(); if (!policy.isExecApproved(caller)) { ViolationMetricsReporter.incrementExec(); @@ -83,7 +93,7 @@ static void checkExec(String target) { target, caller, policy.enforcementMode()); - if (policy.enforcementMode() == SolrSecurityPolicy.EnforcementMode.ENFORCE) { + if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { throw new SecurityException( "Process spawning denied by Solr security agent — unapproved caller: " + caller); } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/RuntimeHaltInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/RuntimeHaltInterceptor.java new file mode 100644 index 000000000000..513e1ea9ce89 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/RuntimeHaltInterceptor.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.lang.StackWalker.Option; +import java.util.Collection; +import net.bytebuddy.asm.Advice; + +/** + * ByteBuddy {@link Advice} interceptor for {@link Runtime#halt(int)}. + * + *

    Identical logic to {@link SystemExitInterceptor}; registered separately because ByteBuddy + * requires one advice class per instrumented method. + * + *

    This file was derived from the OpenSearch project and modified. See {@code NOTICE.txt} for + * attribution. + */ +public class RuntimeHaltInterceptor { + + private RuntimeHaltInterceptor() {} + + @Advice.OnMethodEnter + public static void intercept(@Advice.Argument(0) int code) throws Exception { + if (!AgentPolicy.isInitialized()) return; + final AgentPolicy policy = AgentPolicy.getInstance(); + + final StackWalker walker = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE); + final Class caller = walker.getCallerClass(); + final Collection> chain = walker.walk(StackCallerClassChainExtractor.INSTANCE); + + if (!policy.isChainThatCanExit(chain)) { + ViolationMetricsReporter.incrementExit(); + SecurityViolationLogger.log( + SecurityViolationLogger.ViolationType.SYSTEM_EXIT, + "halt(" + code + ")", + caller.getName(), + policy.enforcementMode()); + if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException( + "The class " + + caller.getName() + + " is not allowed to call Runtime::halt(" + + code + + ")"); + } + } + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java index 8dee74b8d3ea..3c3150910d43 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java @@ -29,10 +29,10 @@ * SECURITY VIOLATION [TYPE] target= caller= mode= source= * } * - *

    In {@link SolrSecurityPolicy.EnforcementMode#WARN warn mode} entries are logged at {@code - * WARN} level and the operation is allowed to proceed. In {@link - * SolrSecurityPolicy.EnforcementMode#ENFORCE enforce mode} entries are logged at {@code ERROR} - * level and the operation must be blocked by the calling interceptor. + *

    In {@link AgentPolicy.EnforcementMode#WARN warn mode} entries are logged at {@code WARN} level + * and the operation is allowed to proceed. In {@link AgentPolicy.EnforcementMode#ENFORCE enforce + * mode} entries are logged at {@code ERROR} level and the operation must be blocked by the calling + * interceptor. * *

    The {@code source} field identifies whether the matching policy entry (if any) came from the * default bundled policy ({@code DEFAULT}) or from an operator extension ({@code OPERATOR}). For @@ -74,12 +74,12 @@ public static void log( ViolationType type, String target, String caller, - SolrSecurityPolicy.EnforcementMode mode, + AgentPolicy.EnforcementMode mode, String source) { String message = buildMessage(type, target, caller, mode, source); - if (mode == SolrSecurityPolicy.EnforcementMode.ENFORCE) { + if (mode == AgentPolicy.EnforcementMode.ENFORCE) { log.error(message); if (log.isDebugEnabled()) { log.debug( @@ -99,15 +99,15 @@ public static void log( * source tagging is available). */ public static void log( - ViolationType type, String target, String caller, SolrSecurityPolicy.EnforcementMode mode) { + ViolationType type, String target, String caller, AgentPolicy.EnforcementMode mode) { log(type, target, caller, mode, null); } - static String buildMessage( + public static String buildMessage( ViolationType type, String target, String caller, - SolrSecurityPolicy.EnforcementMode mode, + AgentPolicy.EnforcementMode mode, String source) { StringBuilder sb = new StringBuilder(); diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java new file mode 100644 index 000000000000..b9f3b8c3bf52 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.UnixDomainSocketAddress; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.asm.Advice.Origin; + +/** + * ByteBuddy {@link Advice} interceptor for outbound socket connections. + * + *

    This file was derived from the OpenSearch project and modified. See {@code NOTICE.txt} for + * attribution. + */ +public class SocketChannelInterceptor { + + /** SocketChannelInterceptor */ + public SocketChannelInterceptor() {} + + /** + * Interceptors + * + * @param args arguments + * @param method method + * @throws Exception exceptions + */ + @Advice.OnMethodEnter + public static void intercept(@Advice.AllArguments Object[] args, @Origin Method method) + throws Exception { + if (!AgentPolicy.isInitialized()) return; + final AgentPolicy policy = AgentPolicy.getInstance(); + + final StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); + final String caller = walker.getCallerClass().getName(); + + if (args[0] instanceof InetSocketAddress address) { + if (!policy.trustedHosts().contains(address.getHostString())) { + final String host = address.getHostString(); + final int port = address.getPort(); + + if (!isEndpointPermitted(policy, host, port)) { + final String target = host + ":" + port; + ViolationMetricsReporter.incrementNetwork(); + SecurityViolationLogger.log( + SecurityViolationLogger.ViolationType.NETWORK_CONNECT, + target, + caller, + policy.enforcementMode()); + if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException( + "Outbound network connection denied by Solr security agent: " + target); + } + } + } + } else if (args[0] instanceof UnixDomainSocketAddress) { + // Unix domain socket — local IPC, always allow + return; + } + } + + // --------------------------------------------------------------------------- + // Static helpers (used by advice and by tests) + // --------------------------------------------------------------------------- + + /** + * Checks whether the given remote address may be connected to under the active policy. Increments + * the network violation counter and logs on violation; throws {@link SecurityException} in + * enforce mode. + * + *

    Used by tests to exercise the network check without ByteBuddy instrumentation. + */ + public static void checkConnect(java.net.InetSocketAddress address) { + if (!AgentPolicy.isInitialized()) return; + if (address.isUnresolved()) return; + AgentPolicy policy = AgentPolicy.getInstance(); + if (policy.trustedHosts().contains(address.getHostString())) return; + String caller = topCallerClassName(); + String host = address.getHostString(); + int port = address.getPort(); + if (!isEndpointPermitted(policy, host, port)) { + String target = host + ":" + port; + ViolationMetricsReporter.incrementNetwork(); + SecurityViolationLogger.log( + SecurityViolationLogger.ViolationType.NETWORK_CONNECT, + target, + caller, + policy.enforcementMode()); + if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException( + "Outbound network connection denied by Solr security agent: " + target); + } + } + } + + public static String topCallerClassName() { + try { + return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .getCallerClass() + .getName(); + } catch (Exception e) { + return ""; + } + } + + // --------------------------------------------------------------------------- + // Endpoint matching helpers + // --------------------------------------------------------------------------- + + /** + * Returns {@code true} if at least one permitted endpoint entry in the policy covers the given + * host and port. Matching rules: + * + *

      + *
    • Entry {@code *:port} — matches any host on that exact port + *
    • Entry {@code host:port} — matches exact host and port + *
    • Entry {@code host:low-high} — matches the host with a port in the inclusive range + *
    • Entry {@code *} (no colon) — matches everything (broad wildcard) + *
    + */ + public static boolean isEndpointPermitted(AgentPolicy policy, String host, int port) { + for (PermittedEndpoint entry : policy.permittedEndpoints()) { + if (entry.codeBase() != null) continue; + if (matchesEndpoint(entry.hostPort(), host, port)) return true; + } + return false; + } + + public static boolean matchesEndpoint(String hostPortEntry, String host, int port) { + if ("*".equals(hostPortEntry)) return true; + + int colonIdx = hostPortEntry.lastIndexOf(':'); + if (colonIdx < 0) { + return matchesHost(hostPortEntry, host); + } + + String entryHost = hostPortEntry.substring(0, colonIdx); + String entryPort = hostPortEntry.substring(colonIdx + 1); + + if (!matchesHost(entryHost, host)) return false; + return matchesPort(entryPort, port); + } + + public static boolean matchesHost(String entryHost, String actualHost) { + if ("*".equals(entryHost)) return true; + return entryHost.equalsIgnoreCase(actualHost); + } + + public static boolean matchesPort(String entryPort, int actualPort) { + if ("*".equals(entryPort)) return true; + int dashIdx = entryPort.indexOf('-'); + if (dashIdx < 0) { + try { + return Integer.parseInt(entryPort.trim()) == actualPort; + } catch (NumberFormatException e) { + return false; + } + } + try { + int low = Integer.parseInt(entryPort.substring(0, dashIdx).trim()); + int high = Integer.parseInt(entryPort.substring(dashIdx + 1).trim()); + return actualPort >= low && actualPort <= high; + } catch (NumberFormatException e) { + return false; + } + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java index 15b52b8bcf57..89f506d57cc4 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java @@ -17,9 +17,13 @@ package org.apache.solr.security.agent; import java.lang.instrument.Instrumentation; +import java.nio.channels.FileChannel; import java.nio.file.Path; +import java.nio.file.spi.FileSystemProvider; +import net.bytebuddy.ByteBuddy; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; +import net.bytebuddy.implementation.Implementation; import net.bytebuddy.matcher.ElementMatchers; /** @@ -35,7 +39,7 @@ *
      *
    1. Locate and parse {@code agent-security.policy} (and the optional {@code * agent-security-extra.policy}) via {@link PolicyLoader}. - *
    2. Initialize the {@link SolrSecurityPolicy} singleton. + *
    3. Initialize the {@link AgentPolicy} singleton. *
    4. Register all four ByteBuddy interceptors with the JVM instrumentation API. *
    5. If policy loading fails and enforcement mode is {@code ENFORCE}, halt the JVM; in {@code * WARN} mode, log the error and continue without protection. @@ -43,7 +47,7 @@ * *

      Bootstrap injection

      * - * The interceptor classes ({@link FileAccessInterceptor}, etc.) are injected into the bootstrap + * The interceptor classes ({@link FileInterceptor}, etc.) are injected into the bootstrap * classloader using {@code ClassInjector.UsingUnsafe.ofBootLoader()} so that they can intercept JDK * methods which are loaded by the bootstrap loader. The {@code @SuppressForbidden} annotation on * the injection call acknowledges the intentional use of {@code sun.misc.Unsafe}. @@ -87,13 +91,14 @@ private static void bootOtel(Instrumentation inst) { // Locate the default policy file next to the agent JAR. Path defaultPolicyPath = resolveDefaultPolicyPath(); - SolrSecurityPolicy policy = null; + AgentPolicy policy = null; try { PolicyLoader loader = new PolicyLoader(); policy = loader.load(defaultPolicyPath); } catch (IllegalStateException e) { // Policy load failed. - String modeStr = System.getProperty("solr.security.agent.mode", "warn"); + String modeStr = PolicyPropertyExpander.getPropertyOrEnv("solr.security.agent.mode"); + if (modeStr == null) modeStr = "warn"; if ("enforce".equalsIgnoreCase(modeStr.trim())) { System.err.println( "[Solr SecurityAgent] FATAL: Cannot load security policy in enforce mode. " @@ -109,7 +114,7 @@ private static void bootOtel(Instrumentation inst) { } } - SolrSecurityPolicy.initialize(policy); + AgentPolicy.initialize(policy); // Register ByteBuddy interceptors. try { @@ -127,62 +132,92 @@ private static void bootOtel(Instrumentation inst) { + policy.permittedEndpoints().size()); } + private static final String[] FILE_INTERCEPTED_METHODS = { + "write", + "createFile", + "createDirectories", + "createLink", + "copy", + "move", + "newByteChannel", + "delete", + "deleteIfExists", + "read", + "open" + }; + /** - * Installs all four ByteBuddy interceptors using the provided {@link Instrumentation} instance. - * The interceptor classes are injected into the bootstrap classloader so that they can redefine - * JDK methods. + * Installs all ByteBuddy interceptors using the provided {@link Instrumentation} instance. The + * interceptor classes are injected into the bootstrap classloader so that they can redefine JDK + * methods. */ private static void installInterceptors(Instrumentation inst) { - new AgentBuilder.Default() - // Intercept java.nio.file.Files read/copy/move operations → FileAccessInterceptor - .type(ElementMatchers.named("java.nio.file.Files")) + // AgentBuilder configuration for JDK class instrumentation: + // - Implementation.Context.Disabled: required for REDEFINE so ByteBuddy does not try to + // add auxiliary types (forbidden when redefining already-loaded JDK classes). + // - InitializationStrategy.NoOp: skip static initializer injection (same reason). + // - RedefinitionStrategy.REDEFINITION: redefine already-loaded bootstrap classes (Files, + // etc.) + // in-place rather than scheduling a retransformation pass. + final ByteBuddy byteBuddy = + new ByteBuddy().with(Implementation.Context.Disabled.Factory.INSTANCE); + + new AgentBuilder.Default(byteBuddy) + .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE) + .with(AgentBuilder.RedefinitionStrategy.REDEFINITION) + .with(AgentBuilder.TypeStrategy.Default.REDEFINE) + // Override the default ignore filter so we can instrument bootstrap-loaded JDK classes + // in named modules (java.lang, java.nio, etc.). Without this, ByteBuddy 1.18.x silently + // skips those classes due to module-visibility checks in its default ignore rules. + // Also exclude ByteBuddy's own classes to prevent circular instrumentation at startup. + .ignore(ElementMatchers.isSynthetic().or(ElementMatchers.nameStartsWith("net.bytebuddy."))) + // Intercept FileSystemProvider subclasses, java.nio.file.Files, and FileChannel subtypes. + .type( + ElementMatchers.isSubTypeOf(FileSystemProvider.class) + .or(ElementMatchers.named("java.nio.file.Files")) + .or(ElementMatchers.isSubTypeOf(FileChannel.class))) .transform( (builder, type, classLoader, module, domain) -> builder.visit( - Advice.to(FileAccessInterceptor.class) + Advice.to(FileInterceptor.class) .on( - ElementMatchers.named("newInputStream") - .or(ElementMatchers.named("readAllBytes")) - .or(ElementMatchers.named("readString")) - .or(ElementMatchers.named("readAllLines")) - .or(ElementMatchers.named("newOutputStream")) - .or(ElementMatchers.named("write")) - .or(ElementMatchers.named("delete")) - .or(ElementMatchers.named("deleteIfExists")) - .or(ElementMatchers.named("copy")) - .or(ElementMatchers.named("move"))))) - // Intercept SocketChannel.connect(SocketAddress) → NetworkAccessInterceptor + ElementMatchers.namedOneOf(FILE_INTERCEPTED_METHODS) + .or(ElementMatchers.isAbstract())))) + // Intercept SocketChannel / Socket outbound connections → SocketChannelInterceptor .type( - ElementMatchers.named("java.nio.channels.SocketChannel") - .or(ElementMatchers.named("java.net.Socket"))) + ElementMatchers.isSubTypeOf(java.nio.channels.SocketChannel.class) + .or(ElementMatchers.isSubTypeOf(java.net.Socket.class))) .transform( (builder, type, classLoader, module, domain) -> builder.visit( - Advice.to(NetworkAccessInterceptor.class).on(ElementMatchers.named("connect")))) - // Intercept System.exit(int) → ExitInterceptor.onSystemExit - .type(ElementMatchers.named("java.lang.System")) + Advice.to(SocketChannelInterceptor.class) + .on( + ElementMatchers.named("connect") + .and(ElementMatchers.not(ElementMatchers.isAbstract()))))) + // Intercept System.exit(int) → SystemExitInterceptor + .type(ElementMatchers.is(java.lang.System.class)) .transform( (builder, type, classLoader, module, domain) -> - builder.visit(Advice.to(ExitInterceptor.class).on(ElementMatchers.named("exit")))) - // Intercept Runtime.halt(int) → ExitInterceptor.onRuntimeHalt - .type(ElementMatchers.named("java.lang.Runtime")) + builder.visit( + Advice.to(SystemExitInterceptor.class).on(ElementMatchers.named("exit")))) + // Intercept Runtime.halt(int) → RuntimeHaltInterceptor + .type(ElementMatchers.is(java.lang.Runtime.class)) .transform( (builder, type, classLoader, module, domain) -> - builder.visit(Advice.to(ExitInterceptor.class).on(ElementMatchers.named("halt")))) - // Intercept ProcessBuilder.start() → ProcessExecInterceptor.onProcessBuilderStart - .type(ElementMatchers.named("java.lang.ProcessBuilder")) + builder.visit( + Advice.to(RuntimeHaltInterceptor.class).on(ElementMatchers.named("halt")))) + // Intercept ProcessBuilder.start() → ProcessExecInterceptor + .type(ElementMatchers.is(java.lang.ProcessBuilder.class)) .transform( (builder, type, classLoader, module, domain) -> builder.visit( Advice.to(ProcessExecInterceptor.class).on(ElementMatchers.named("start")))) - // Intercept Runtime.exec(String[]) → ProcessExecInterceptor.onRuntimeExec - .type(ElementMatchers.named("java.lang.Runtime")) + // Intercept Runtime.exec(String[]) → ProcessExecInterceptor + .type(ElementMatchers.is(java.lang.Runtime.class)) .transform( (builder, type, classLoader, module, domain) -> builder.visit( Advice.to(ProcessExecInterceptor.class).on(ElementMatchers.named("exec")))) - .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) - .with(AgentBuilder.TypeStrategy.Default.REDEFINE) .installOn(inst); } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/StackCallerClassChainExtractor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/StackCallerClassChainExtractor.java new file mode 100644 index 000000000000..f72c0f607c5b --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/StackCallerClassChainExtractor.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.lang.StackWalker.StackFrame; +import java.util.Collection; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Extracts the set of all non-hidden declaring classes from the current call stack. + * + *

      Used by {@link SystemExitInterceptor} and {@link RuntimeHaltInterceptor} to obtain the full + * caller chain for exit-permission checks. + * + *

      This file was derived from the OpenSearch project and modified. See {@code NOTICE.txt} for + * attribution. + */ +public final class StackCallerClassChainExtractor + implements Function, Collection>> { + + /** Single instance of stateless class. */ + public static final StackCallerClassChainExtractor INSTANCE = + new StackCallerClassChainExtractor(); + + private StackCallerClassChainExtractor() {} + + @Override + public Collection> apply(Stream frames) { + return cast(frames); + } + + @SuppressWarnings("unchecked") + private static Set cast(Stream frames) { + return (Set) + frames + .map(StackFrame::getDeclaringClass) + .filter(c -> !c.isHidden()) + .collect(Collectors.toSet()); + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/StackInspector.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/StackInspector.java deleted file mode 100644 index 44d227766167..000000000000 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/StackInspector.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.solr.security.agent; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -/** - * Walks the current call stack and returns the ordered list of non-JDK caller classes. - * - *

      This class uses {@link StackWalker#getInstance(StackWalker.Option)} with {@code - * StackWalker.Option#RETAIN_CLASS_REFERENCE} to obtain live {@link Class} objects for each frame. - * JDK-internal frames (whose class is loaded from the {@code jrt:/} location) are filtered out. - * - *

      Virtual thread compatibility

      - * - * {@code StackWalker} is virtual-thread–safe by specification. Unlike deprecated {@code - * Thread.currentThread()} or {@code ThreadGroup} approaches, this implementation does not assume - * thread identity, making enforcement decisions correct for both platform threads and Project Loom - * virtual threads. - */ -public final class StackInspector { - - /** - * StackWalker with class references retained. May be {@code null} if the security environment - * (e.g. Java SecurityManager in tests) denies the {@code getStackWalkerWithClassReference} - * permission — in that case caller identification returns {@code ""}. - */ - private static final StackWalker WALKER; - - static { - StackWalker w; - try { - w = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); - } catch (SecurityException e) { - // Java SecurityManager denied the RuntimePermission "getStackWalkerWithClassReference". - // This can happen in constrained test environments. Fall back to null; all caller-based - // checks will treat the caller as "" (no class reference available). - w = null; - } - WALKER = w; - } - - /** - * The agent's own interceptor and infrastructure classes. These are always on the call stack when - * enforcement is triggered and must be excluded from caller identification. - */ - private static final Set AGENT_INFRASTRUCTURE = - Set.of( - "org.apache.solr.security.agent.StackInspector", - "org.apache.solr.security.agent.FileAccessInterceptor", - "org.apache.solr.security.agent.NetworkAccessInterceptor", - "org.apache.solr.security.agent.ExitInterceptor", - "org.apache.solr.security.agent.ProcessExecInterceptor", - "org.apache.solr.security.agent.SolrSecurityPolicy", - "org.apache.solr.security.agent.SecurityViolationLogger", - "org.apache.solr.security.agent.ViolationMetricsReporter", - "org.apache.solr.security.agent.PolicyLoader", - "org.apache.solr.security.agent.SolrAgentEntryPoint"); - - private StackInspector() {} - - /** - * Returns an ordered list of non-JDK caller classes, starting from the immediate caller of the - * intercepted method and working up the call chain. JDK classes (those loaded from {@code jrt:/} - * or whose class loader is {@code null} — i.e. the bootstrap loader) and ByteBuddy-generated - * classes are excluded. - * - * @return caller classes in call-chain order (innermost first), never {@code null} - */ - public static List> callerClasses() { - if (WALKER == null) return List.of(); - return WALKER.walk( - frames -> { - List> callers = new ArrayList<>(); - frames.forEach( - frame -> { - Class cls = frame.getDeclaringClass(); - if (!isJdkClass(cls) && !isBytebuddyClass(cls) && !isAgentClass(cls)) { - callers.add(cls); - } - }); - return callers; - }); - } - - /** - * Returns the fully-qualified name of the first non-JDK, non-ByteBuddy class in the call stack, - * or {@code ""} if none is found. - */ - public static String topCallerClassName() { - if (WALKER == null) return ""; - return WALKER.walk( - frames -> - frames - .filter( - f -> - !isJdkClass(f.getDeclaringClass()) - && !isBytebuddyClass(f.getDeclaringClass()) - && !isAgentClass(f.getDeclaringClass())) - .findFirst() - .map(f -> f.getDeclaringClass().getName()) - .orElse("")); - } - - /** - * Returns {@code true} if the class should be considered a JDK internal frame and excluded from - * caller analysis. This covers: - * - *
        - *
      • Classes whose classloader is {@code null} (bootstrap loader — Java platform classes) - *
      • Classes from the {@code java.*}, {@code javax.*}, {@code sun.*}, {@code jdk.*} packages - *
      - */ - static boolean isJdkClass(Class cls) { - if (cls.getClassLoader() == null) return true; - String name = cls.getName(); - return name.startsWith("java.") - || name.startsWith("javax.") - || name.startsWith("sun.") - || name.startsWith("jdk.") - || name.startsWith("com.sun."); - } - - /** - * Returns {@code true} if this class is one of the agent's own interceptor or infrastructure - * classes that are always on the call stack when enforcement is triggered and must not be - * reported as the "caller". This uses an explicit allowlist so that test classes in the same - * package are not inadvertently excluded. - */ - static boolean isAgentClass(Class cls) { - return AGENT_INFRASTRUCTURE.contains(cls.getName()); - } - - /** - * Returns {@code true} if this class is a ByteBuddy-generated instrumentation proxy that should - * not appear in violation call-site analysis. - */ - static boolean isBytebuddyClass(Class cls) { - String name = cls.getName(); - return name.contains("$ByteBuddy$") || name.startsWith("net.bytebuddy."); - } -} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SystemExitInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SystemExitInterceptor.java new file mode 100644 index 000000000000..8b328ec09dc5 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SystemExitInterceptor.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.lang.StackWalker.Option; +import java.util.Collection; +import net.bytebuddy.asm.Advice; + +/** + * ByteBuddy {@link Advice} interceptor for {@link System#exit(int)}. + * + *

      Uses the full call-chain check: any class in the stack that matches an approved exit-caller + * pattern in {@link AgentPolicy} grants permission. In {@link AgentPolicy.EnforcementMode#WARN warn + * mode} a violation is logged but the exit is allowed to proceed; in {@link + * AgentPolicy.EnforcementMode#ENFORCE enforce mode} a {@link SecurityException} is thrown. + * + *

      This file was derived from the OpenSearch project and modified. See {@code NOTICE.txt} for + * attribution. + */ +public class SystemExitInterceptor { + + private SystemExitInterceptor() {} + + @Advice.OnMethodEnter + public static void intercept(@Advice.Argument(0) int code) throws Exception { + if (!AgentPolicy.isInitialized()) return; + final AgentPolicy policy = AgentPolicy.getInstance(); + + final StackWalker walker = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE); + final Class caller = walker.getCallerClass(); + final Collection> chain = walker.walk(StackCallerClassChainExtractor.INSTANCE); + + if (!policy.isChainThatCanExit(chain)) { + ViolationMetricsReporter.incrementExit(); + SecurityViolationLogger.log( + SecurityViolationLogger.ViolationType.SYSTEM_EXIT, + "exit(" + code + ")", + caller.getName(), + policy.enforcementMode()); + if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException( + "The class " + caller.getName() + " is not allowed to call System::exit(" + code + ")"); + } + } + } +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java index e4a7eec17d22..5227c5e2aeb3 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java @@ -20,24 +20,48 @@ * *

      This package contains the Java agent entry point and runtime enforcement infrastructure that * replaces the removed Java Security Manager API. The agent intercepts file access, outbound - * network connections, {@code System.exit()}, and process spawning at the bytecode level, enforcing - * a policy loaded from JDK-style {@code .policy} files. + * network connections, {@code System.exit()}, {@code Runtime.halt()}, and process spawning at the + * bytecode level, enforcing a policy loaded from JDK-style {@code .policy} files. * - *

      Key classes: + *

      Key classes

      * *
        *
      • {@link org.apache.solr.security.agent.SolrAgentEntryPoint} — {@code premain()}/{@code - * agentmain()} entry point; registers all ByteBuddy interceptors. - *
      • {@link org.apache.solr.security.agent.PolicyLoader} — parses {@code .policy} files with - * Solr variable substitution. - *
      • {@link org.apache.solr.security.agent.SolrSecurityPolicy} — immutable singleton holding the - * merged default + operator policy. - *
      • {@link org.apache.solr.security.agent.StackInspector} — virtual-thread-safe call chain - * analysis via {@code StackWalker}. + * agentmain()} entry point; loads the policy and registers all ByteBuddy interceptors. + *
      • {@link org.apache.solr.security.agent.AgentPolicy} — immutable singleton holding the merged + * default + operator policy, enforcement mode, and trusted-host set. + *
      • {@link org.apache.solr.security.agent.PolicyLoader} — parses {@code .policy} files, + * performs variable substitution, and merges the default and operator extension files. + *
      • {@link org.apache.solr.security.agent.PolicyPropertyExpander} — expands {@code ${property}} + * placeholders; falls back to env vars using the standard {@code SOLR_FOO_BAR} convention + * (with custom overrides for non-standard mappings such as {@code SOLR_HOME}). + *
      • {@link org.apache.solr.security.agent.PolicyFileParser} — {@code StreamTokenizer}-based + * parser for JDK-style {@code .policy} files; handles comments and quoted strings natively. + *
      • {@link org.apache.solr.security.agent.StackCallerClassChainExtractor} — virtual-thread-safe + * call chain extraction via {@code StackWalker}. *
      • {@link org.apache.solr.security.agent.SecurityViolationLogger} — structured SLF4J violation * log emitter. *
      • {@link org.apache.solr.security.agent.ViolationMetricsReporter} — per-type violation * counters with deferred registration in {@code SolrMetricManager}. *
      + * + *

      OpenSearch-derived files

      + * + *

      The following source files in this package were derived from the OpenSearch project (Apache License + * 2.0) and modified to integrate with Solr's policy model: + * + *

        + *
      • {@link org.apache.solr.security.agent.FileInterceptor} + *
      • {@link org.apache.solr.security.agent.SocketChannelInterceptor} + *
      • {@link org.apache.solr.security.agent.SystemExitInterceptor} + *
      • {@link org.apache.solr.security.agent.RuntimeHaltInterceptor} + *
      • {@link org.apache.solr.security.agent.StackCallerClassChainExtractor} + *
      • {@link org.apache.solr.security.agent.PolicyFileParser} + *
      • {@link org.apache.solr.security.agent.PolicyTokenStream} + *
      • {@link org.apache.solr.security.agent.PolicyPropertyExpander} + *
      + * + *

      See {@code NOTICE.txt} for full attribution. */ package org.apache.solr.security.agent; diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/ExitInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/ExitInterceptorTest.java deleted file mode 100644 index 69f805eafbc9..000000000000 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/ExitInterceptorTest.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.solr.security.agent; - -import java.util.List; -import org.apache.solr.SolrTestCase; -import org.junit.Before; -import org.junit.Test; - -/** Unit tests for {@link ExitInterceptor} enforcement logic. */ -public class ExitInterceptorTest extends SolrTestCase { - - private long exitCountBefore; - - @Before - public void snapshotCounters() { - exitCountBefore = ViolationMetricsReporter.exitCount(); - } - - private void initPolicy( - boolean approved, String callerClass, SolrSecurityPolicy.EnforcementMode mode) { - // Reset singleton if already set - resetPolicySingleton(); - List exitCallers = - approved - ? List.of( - new ApprovedCallSite( - callerClass, - ApprovedCallSite.Operation.EXIT, - PolicyLoader.PolicySource.DEFAULT)) - : List.of(); - SolrSecurityPolicy policy = - new SolrSecurityPolicy(List.of(), List.of(), exitCallers, List.of(), mode); - SolrSecurityPolicy.initialize(policy); - } - - /** Resets the static singleton so each test starts fresh. */ - private static void resetPolicySingleton() { - SolrSecurityPolicy.resetForTesting(); - } - - @Test - public void testApprovedCallerDoesNotIncreaseCounter() { - // Use the test class itself as an approved caller - initPolicy( - true, ExitInterceptorTest.class.getName(), SolrSecurityPolicy.EnforcementMode.ENFORCE); - // Directly exercise checkExit with the approved class on the call stack - // We call checkExit with a simulated target — no counter should increment - ExitInterceptor.checkExit("System.exit(0)"); - assertEquals(exitCountBefore, ViolationMetricsReporter.exitCount()); - resetPolicySingleton(); - } - - @Test - public void testUnapprovedCallerInWarnModeIncrementsCounter() { - initPolicy(false, "some.other.Class", SolrSecurityPolicy.EnforcementMode.WARN); - ExitInterceptor.checkExit("System.exit(0)"); - assertEquals(exitCountBefore + 1, ViolationMetricsReporter.exitCount()); - resetPolicySingleton(); - } - - @Test(expected = SecurityException.class) - public void testUnapprovedCallerInEnforceModeThrows() { - initPolicy(false, "some.other.Class", SolrSecurityPolicy.EnforcementMode.ENFORCE); - try { - ExitInterceptor.checkExit("System.exit(0)"); - } finally { - resetPolicySingleton(); - } - } - - @Test - public void testUnapprovedCallerInEnforceModeIncrementsCounter() { - initPolicy(false, "some.other.Class", SolrSecurityPolicy.EnforcementMode.ENFORCE); - long before = ViolationMetricsReporter.exitCount(); - try { - ExitInterceptor.checkExit("System.exit(0)"); - } catch (SecurityException ignored) { - // expected - } - assertEquals(before + 1, ViolationMetricsReporter.exitCount()); - resetPolicySingleton(); - } - - @Test - public void testRuntimeHaltCallsCheckExit() { - initPolicy(false, "nobody", SolrSecurityPolicy.EnforcementMode.WARN); - long before = ViolationMetricsReporter.exitCount(); - ExitInterceptor.checkExit("Runtime.halt(0)"); // same code path - assertEquals(before + 1, ViolationMetricsReporter.exitCount()); - resetPolicySingleton(); - } -} diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/NetworkAccessInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/NetworkAccessInterceptorTest.java deleted file mode 100644 index b47178985279..000000000000 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/NetworkAccessInterceptorTest.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.solr.security.agent; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.util.List; -import org.apache.solr.SolrTestCase; -import org.junit.Test; - -/** Unit tests for {@link NetworkAccessInterceptor} policy matching logic. */ -public class NetworkAccessInterceptorTest extends SolrTestCase { - - private SolrSecurityPolicy policyWithEndpoint(String hostPort) { - PermittedEndpoint ep = - new PermittedEndpoint(hostPort, "connect,resolve", null, PolicyLoader.PolicySource.DEFAULT); - return new SolrSecurityPolicy( - List.of(), List.of(ep), List.of(), List.of(), SolrSecurityPolicy.EnforcementMode.ENFORCE); - } - - @Test - public void testLoopbackPermittedUnconditionally() throws Exception { - // Even with an empty policy, loopback should be allowed. - SolrSecurityPolicy policy = - new SolrSecurityPolicy( - List.of(), List.of(), List.of(), List.of(), SolrSecurityPolicy.EnforcementMode.ENFORCE); - - InetSocketAddress loopback = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 8983); - // Should not throw — loopback is unconditionally permitted - assertTrue(loopback.getAddress().isLoopbackAddress()); - // checkConnect skips loopback before policy lookup - } - - @Test - public void testExactHostPortPermitted() { - SolrSecurityPolicy policy = policyWithEndpoint("192.168.1.100:8983"); - assertTrue(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.100", 8983)); - assertFalse(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.100", 8984)); - assertFalse(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.101", 8983)); - } - - @Test - public void testWildcardHostPortPermitted() { - // *:8983 should match any host on port 8983 - SolrSecurityPolicy policy = policyWithEndpoint("*:8983"); - assertTrue(NetworkAccessInterceptor.isEndpointPermitted(policy, "10.0.0.5", 8983)); - assertTrue(NetworkAccessInterceptor.isEndpointPermitted(policy, "some-other-host", 8983)); - assertFalse(NetworkAccessInterceptor.isEndpointPermitted(policy, "10.0.0.5", 9983)); - } - - @Test - public void testPortRangePermitted() { - SolrSecurityPolicy policy = policyWithEndpoint("192.168.1.1:8000-9000"); - assertTrue(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.1", 8983)); - assertTrue(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.1", 8000)); - assertTrue(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.1", 9000)); - assertFalse(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.1", 7999)); - assertFalse(NetworkAccessInterceptor.isEndpointPermitted(policy, "192.168.1.1", 9001)); - } - - @Test - public void testCodebaseScopedEntrySkipped() { - // codeBase-scoped entries should be ignored in the path-based check - PermittedEndpoint codeBasedEp = - new PermittedEndpoint( - "*", - "connect,resolve", - "file:/opt/solr/modules/jwt-auth/-", - PolicyLoader.PolicySource.DEFAULT); - SolrSecurityPolicy policy = - new SolrSecurityPolicy( - List.of(), - List.of(codeBasedEp), - List.of(), - List.of(), - SolrSecurityPolicy.EnforcementMode.ENFORCE); - // Should return false because the only entry is codeBase-scoped - assertFalse(NetworkAccessInterceptor.isEndpointPermitted(policy, "external.host", 443)); - } - - @Test - public void testUnlistedHostPortBlocked() { - SolrSecurityPolicy policy = policyWithEndpoint("localhost:8983"); - assertFalse(NetworkAccessInterceptor.isEndpointPermitted(policy, "10.0.0.1", 443)); - } - - @Test - public void testBroadWildcardPermitsAll() { - SolrSecurityPolicy policy = policyWithEndpoint("*"); - assertTrue(NetworkAccessInterceptor.isEndpointPermitted(policy, "anything.com", 443)); - } -} diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java index 0b3a4b842160..95109cfa22ae 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java @@ -60,7 +60,7 @@ private Path writeDefaultPolicy(Path dir) throws Exception { return policy; } - private SolrSecurityPolicy loadWithExtra(Path defaultPolicy, Path extraPolicy) { + private AgentPolicy loadWithExtra(Path defaultPolicy, Path extraPolicy) { if (extraPolicy != null) { System.setProperty("solr.security.agent.extra.policy", extraPolicy.toString()); } @@ -86,7 +86,7 @@ public void testExtraPolicyPathIsPermitted() throws Exception { + "};\n", StandardCharsets.UTF_8); - SolrSecurityPolicy policy = loadWithExtra(defaultPolicy, extraPolicy); + AgentPolicy policy = loadWithExtra(defaultPolicy, extraPolicy); assertTrue(policy.isPathPermitted(tmpDir.resolve("data.txt").toString(), "read")); } @@ -105,7 +105,7 @@ public void testUnlistedPathStillBlockedWhenExtraPolicyPresent() throws Exceptio + "};\n", StandardCharsets.UTF_8); - SolrSecurityPolicy policy = loadWithExtra(defaultPolicy, extraPolicy); + AgentPolicy policy = loadWithExtra(defaultPolicy, extraPolicy); // /etc is not in either policy assertFalse(policy.isPathPermitted("/etc/shadow", "read")); } @@ -125,7 +125,7 @@ public void testExtraPolicyEntriesTaggedOperator() throws Exception { + "};\n", StandardCharsets.UTF_8); - SolrSecurityPolicy policy = loadWithExtra(defaultPolicy, extraPolicy); + AgentPolicy policy = loadWithExtra(defaultPolicy, extraPolicy); List paths = policy.permittedPaths(); boolean hasOperator = paths.stream().anyMatch(p -> p.source() == PolicyLoader.PolicySource.OPERATOR); @@ -137,7 +137,7 @@ public void testDefaultPolicyEntriesTaggedDefault() throws Exception { Path tmpDir = createTempDir(); Path defaultPolicy = writeDefaultPolicy(tmpDir); - SolrSecurityPolicy policy = loadWithExtra(defaultPolicy, null); + AgentPolicy policy = loadWithExtra(defaultPolicy, null); List paths = policy.permittedPaths(); boolean hasDefault = paths.stream().anyMatch(p -> p.source() == PolicyLoader.PolicySource.DEFAULT); @@ -158,7 +158,7 @@ public void testExtraPolicyAbsentIsNonFatal() throws Exception { "solr.security.agent.extra.policy", tmpDir.resolve("nonexistent.policy").toString()); // Should not throw; default policy still loads - SolrSecurityPolicy policy = new PolicyLoader().load(defaultPolicy); + AgentPolicy policy = new PolicyLoader().load(defaultPolicy); assertTrue(policy.isPathPermitted("/opt/solr/conf", "read")); } @@ -178,7 +178,7 @@ public void testMalformedExtraPolicyIsSkippedGracefully() throws Exception { System.setProperty("solr.security.agent.extra.policy", extraPolicy.toString()); // Should not throw; default policy still loads - SolrSecurityPolicy policy = new PolicyLoader().load(defaultPolicy); + AgentPolicy policy = new PolicyLoader().load(defaultPolicy); // Default policy (/opt/solr) is still active assertTrue(policy.isPathPermitted("/opt/solr/conf", "read")); // Malformed extra policy adds no new paths @@ -197,7 +197,7 @@ public void testViolationLogIncludesSourceField() { SecurityViolationLogger.ViolationType.FILE_READ, "/tmp/secret.txt", "com.example.Caller", - SolrSecurityPolicy.EnforcementMode.WARN, + AgentPolicy.EnforcementMode.WARN, "OPERATOR"); assertTrue("Expected source=OPERATOR in log message", msg.contains("source=OPERATOR")); } @@ -209,7 +209,7 @@ public void testViolationLogOmitsSourceWhenNull() { SecurityViolationLogger.ViolationType.FILE_READ, "/tmp/secret.txt", "com.example.Caller", - SolrSecurityPolicy.EnforcementMode.WARN, + AgentPolicy.EnforcementMode.WARN, null); assertFalse("Expected no source= field when source is null", msg.contains("source=")); } diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java index 7ea5765cc700..39f823ba0512 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java @@ -28,53 +28,46 @@ public class PolicyLoaderTest extends SolrTestCase { // --------------------------------------------------------------------------- - // Variable substitution + // Variable expansion (PolicyPropertyExpander) // --------------------------------------------------------------------------- @Test - public void testSubstituteKnownSystemProperties() { - System.setProperty("solr.home", "/opt/solr"); - String result = PolicyLoader.substituteVariables("path=${solr.home}"); - assertEquals("path=/opt/solr", result); + public void testExpandKnownSystemProperty() throws Exception { + System.setProperty("solr.solr.home", "/opt/solr"); + assertEquals("/opt/solr", PolicyPropertyExpander.expand("${solr.solr.home}")); } @Test - public void testSolrPortSubstitution() { - System.setProperty("solr.port", "8983"); - String result = PolicyLoader.substituteVariables("*:${solr.port}"); - assertEquals("*:8983", result); + public void testExpandSolrPort() throws Exception { + System.setProperty("solr.port.listen", "8983"); + assertEquals("*:8983", PolicyPropertyExpander.expand("*:${solr.port.listen}")); } @Test - public void testSolrZkPortDefaultsToSolrPortPlusOneThousand() { - System.setProperty("solr.port", "8983"); + public void testSolrZkPortDefaultsToSolrPortPlusOneThousand() throws Exception { + System.setProperty("solr.port.listen", "8983"); System.clearProperty("solr.zk.port"); - String result = PolicyLoader.substituteVariables("*:${solr.zk.port}"); - assertEquals("*:9983", result); + assertEquals("*:9983", PolicyPropertyExpander.expand("*:${solr.zk.port}")); } @Test - public void testSolrZkPortExplicitOverride() { + public void testSolrZkPortExplicitOverride() throws Exception { System.setProperty("solr.zk.port", "2181"); try { - String result = PolicyLoader.substituteVariables("*:${solr.zk.port}"); - assertEquals("*:2181", result); + assertEquals("*:2181", PolicyPropertyExpander.expand("*:${solr.zk.port}")); } finally { System.clearProperty("solr.zk.port"); } } @Test - public void testSolrInstallDirSubstitution() { - System.setProperty("solr.install.dir", "/opt/solr"); - String result = PolicyLoader.substituteVariables("file:${solr.install.dir}/modules/jwt-auth/-"); - assertEquals("file:/opt/solr/modules/jwt-auth/-", result); + public void testExpandNullReturnsNull() throws Exception { + assertNull(PolicyPropertyExpander.expand(null)); } - @Test - public void testUnknownVariableLeftAsIs() { - String result = PolicyLoader.substituteVariables("path=${unknown.var}"); - assertEquals("path=${unknown.var}", result); + @Test(expected = PolicyPropertyExpander.ExpandException.class) + public void testUnknownVariableThrows() throws Exception { + PolicyPropertyExpander.expand("path=${solr.this.property.does.not.exist.xyz}"); } // --------------------------------------------------------------------------- @@ -174,7 +167,7 @@ public void testValidPolicyParsesCorrectly() throws Exception { StandardCharsets.UTF_8); PolicyLoader loader = new PolicyLoader(); - SolrSecurityPolicy policy = loader.load(policyFile); + AgentPolicy policy = loader.load(policyFile); assertFalse(policy.permittedPaths().isEmpty()); assertFalse(policy.permittedEndpoints().isEmpty()); @@ -198,7 +191,7 @@ public void testExtraPolicyAbsentIsNonFatal() throws Exception { System.setProperty( "solr.security.agent.extra.policy", createTempDir().resolve("absent.policy").toString()); try { - SolrSecurityPolicy policy = new PolicyLoader().load(defaultPolicy); + AgentPolicy policy = new PolicyLoader().load(defaultPolicy); // Must succeed — absent extra policy is not an error assertNotNull(policy); } finally { @@ -222,7 +215,7 @@ public void testExtraPolicyMergedAndTaggedOperator() throws Exception { System.setProperty("solr.security.agent.extra.policy", extraPolicy.toString()); try { - SolrSecurityPolicy policy = new PolicyLoader().load(defaultPolicy); + AgentPolicy policy = new PolicyLoader().load(defaultPolicy); // Both paths must be present List paths = policy.permittedPaths(); @@ -246,7 +239,7 @@ public void testRecursivePathFlagSet() throws Exception { "grant { permission java.io.FilePermission \"/data/-\", \"read,write,delete\"; };\n", StandardCharsets.UTF_8); - SolrSecurityPolicy policy = new PolicyLoader().load(policyFile); + AgentPolicy policy = new PolicyLoader().load(policyFile); PermittedPath path = policy.permittedPaths().get(0); assertEquals("/data", path.path()); assertTrue(path.recursive()); diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java index e2f0d8cc9e88..5136aadf1f3e 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java @@ -31,8 +31,7 @@ public void snapshotCounters() { execCountBefore = ViolationMetricsReporter.execCount(); } - private void initPolicy( - boolean approved, String callerClass, SolrSecurityPolicy.EnforcementMode mode) { + private void initPolicy(boolean approved, String callerClass, AgentPolicy.EnforcementMode mode) { resetPolicySingleton(); List execCallers = approved @@ -42,21 +41,18 @@ private void initPolicy( ApprovedCallSite.Operation.EXEC, PolicyLoader.PolicySource.DEFAULT)) : List.of(); - SolrSecurityPolicy policy = - new SolrSecurityPolicy(List.of(), List.of(), List.of(), execCallers, mode); - SolrSecurityPolicy.initialize(policy); + AgentPolicy policy = new AgentPolicy(List.of(), List.of(), List.of(), execCallers, mode); + AgentPolicy.initialize(policy); } private static void resetPolicySingleton() { - SolrSecurityPolicy.resetForTesting(); + AgentPolicy.resetForTesting(); } @Test public void testApprovedCallerDoesNotIncreaseCounter() { initPolicy( - true, - ProcessExecInterceptorTest.class.getName(), - SolrSecurityPolicy.EnforcementMode.ENFORCE); + true, ProcessExecInterceptorTest.class.getName(), AgentPolicy.EnforcementMode.ENFORCE); ProcessExecInterceptor.checkExec("ProcessBuilder.start()"); assertEquals(execCountBefore, ViolationMetricsReporter.execCount()); resetPolicySingleton(); @@ -64,7 +60,7 @@ public void testApprovedCallerDoesNotIncreaseCounter() { @Test public void testUnapprovedCallerInWarnModeIncrementsCounter() { - initPolicy(false, "nobody", SolrSecurityPolicy.EnforcementMode.WARN); + initPolicy(false, "nobody", AgentPolicy.EnforcementMode.WARN); ProcessExecInterceptor.checkExec("ProcessBuilder.start()"); assertEquals(execCountBefore + 1, ViolationMetricsReporter.execCount()); resetPolicySingleton(); @@ -72,7 +68,7 @@ public void testUnapprovedCallerInWarnModeIncrementsCounter() { @Test(expected = SecurityException.class) public void testUnapprovedCallerInEnforceModeThrows() { - initPolicy(false, "nobody", SolrSecurityPolicy.EnforcementMode.ENFORCE); + initPolicy(false, "nobody", AgentPolicy.EnforcementMode.ENFORCE); try { ProcessExecInterceptor.checkExec("ProcessBuilder.start()"); } finally { @@ -82,7 +78,7 @@ public void testUnapprovedCallerInEnforceModeThrows() { @Test public void testRuntimeExecBlocked() { - initPolicy(false, "nobody", SolrSecurityPolicy.EnforcementMode.WARN); + initPolicy(false, "nobody", AgentPolicy.EnforcementMode.WARN); long before = ViolationMetricsReporter.execCount(); ProcessExecInterceptor.checkExec("Runtime.exec(ls)"); assertEquals(before + 1, ViolationMetricsReporter.execCount()); @@ -91,7 +87,7 @@ public void testRuntimeExecBlocked() { @Test public void testWildcardApprovalMatchesAny() { - initPolicy(true, "*", SolrSecurityPolicy.EnforcementMode.ENFORCE); + initPolicy(true, "*", AgentPolicy.EnforcementMode.ENFORCE); // Should not throw even for an unknown caller ProcessExecInterceptor.checkExec("ProcessBuilder.start()"); assertEquals(execCountBefore, ViolationMetricsReporter.execCount()); diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java new file mode 100644 index 000000000000..0948f433d8ba --- /dev/null +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Set; +import org.apache.solr.SolrTestCase; +import org.junit.After; +import org.junit.Test; + +/** Unit tests for {@link SocketChannelInterceptor} policy matching logic. */ +public class SocketChannelInterceptorTest extends SolrTestCase { + + @After + public void resetSingleton() { + AgentPolicy.resetForTesting(); + } + + private AgentPolicy policyWithEndpoint(String hostPort) { + PermittedEndpoint ep = + new PermittedEndpoint(hostPort, "connect,resolve", null, PolicyLoader.PolicySource.DEFAULT); + return new AgentPolicy( + List.of(), List.of(ep), List.of(), List.of(), AgentPolicy.EnforcementMode.ENFORCE); + } + + @Test + public void testLoopbackPermittedViaTrustedHosts() throws Exception { + AgentPolicy policy = + new AgentPolicy( + List.of(), + List.of(), + List.of(), + List.of(), + AgentPolicy.EnforcementMode.ENFORCE, + Set.of(), + Set.of("127.0.0.1", "localhost", "::1")); + AgentPolicy.initialize(policy); + + InetSocketAddress loopback = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 8983); + assertTrue(policy.trustedHosts().contains(loopback.getHostString())); + // checkConnect skips trusted hosts before policy lookup + SocketChannelInterceptor.checkConnect(loopback); // must not throw + } + + @Test + public void testExactHostPortPermitted() { + AgentPolicy policy = policyWithEndpoint("192.168.1.100:8983"); + assertTrue(SocketChannelInterceptor.isEndpointPermitted(policy, "192.168.1.100", 8983)); + assertFalse(SocketChannelInterceptor.isEndpointPermitted(policy, "192.168.1.100", 8984)); + assertFalse(SocketChannelInterceptor.isEndpointPermitted(policy, "192.168.1.101", 8983)); + } + + @Test + public void testWildcardHostPortPermitted() { + AgentPolicy policy = policyWithEndpoint("*:8983"); + assertTrue(SocketChannelInterceptor.isEndpointPermitted(policy, "10.0.0.5", 8983)); + assertTrue(SocketChannelInterceptor.isEndpointPermitted(policy, "some-other-host", 8983)); + assertFalse(SocketChannelInterceptor.isEndpointPermitted(policy, "10.0.0.5", 9983)); + } + + @Test + public void testPortRangePermitted() { + AgentPolicy policy = policyWithEndpoint("192.168.1.1:8000-9000"); + assertTrue(SocketChannelInterceptor.isEndpointPermitted(policy, "192.168.1.1", 8983)); + assertTrue(SocketChannelInterceptor.isEndpointPermitted(policy, "192.168.1.1", 8000)); + assertTrue(SocketChannelInterceptor.isEndpointPermitted(policy, "192.168.1.1", 9000)); + assertFalse(SocketChannelInterceptor.isEndpointPermitted(policy, "192.168.1.1", 7999)); + assertFalse(SocketChannelInterceptor.isEndpointPermitted(policy, "192.168.1.1", 9001)); + } + + @Test + public void testCodebaseScopedEntrySkipped() { + PermittedEndpoint codeBasedEp = + new PermittedEndpoint( + "*", + "connect,resolve", + "file:/opt/solr/modules/jwt-auth/-", + PolicyLoader.PolicySource.DEFAULT); + AgentPolicy policy = + new AgentPolicy( + List.of(), + List.of(codeBasedEp), + List.of(), + List.of(), + AgentPolicy.EnforcementMode.ENFORCE); + assertFalse(SocketChannelInterceptor.isEndpointPermitted(policy, "external.host", 443)); + } + + @Test + public void testUnlistedHostPortBlocked() { + AgentPolicy policy = policyWithEndpoint("localhost:8983"); + assertFalse(SocketChannelInterceptor.isEndpointPermitted(policy, "10.0.0.1", 443)); + } + + @Test + public void testBroadWildcardPermitsAll() { + AgentPolicy policy = policyWithEndpoint("*"); + assertTrue(SocketChannelInterceptor.isEndpointPermitted(policy, "anything.com", 443)); + } +} diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java index fd069432cbaa..69cc0ce9695e 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java @@ -18,6 +18,7 @@ import java.nio.file.Path; import java.util.List; +import java.util.Set; import org.apache.solr.SolrTestCase; import org.junit.After; import org.junit.Test; @@ -34,18 +35,18 @@ public class SolrAgentIntegrationTest extends SolrTestCase { @After public void resetSingleton() { - SolrSecurityPolicy.resetForTesting(); + AgentPolicy.resetForTesting(); } - private SolrSecurityPolicy buildEnforcePolicy( + private AgentPolicy buildEnforcePolicy( List paths, List endpoints, List exitCallers, List execCallers) { - SolrSecurityPolicy p = - new SolrSecurityPolicy( - paths, endpoints, exitCallers, execCallers, SolrSecurityPolicy.EnforcementMode.ENFORCE); - SolrSecurityPolicy.initialize(p); + AgentPolicy p = + new AgentPolicy( + paths, endpoints, exitCallers, execCallers, AgentPolicy.EnforcementMode.ENFORCE); + AgentPolicy.initialize(p); return p; } @@ -62,8 +63,7 @@ public void testPermittedFileReadSucceeds() { Path target = tmpDir.resolve("test.txt"); // checkPath should not throw for a path inside the permitted dir - FileAccessInterceptor.checkPath( - target, "read", SecurityViolationLogger.ViolationType.FILE_READ); + FileInterceptor.checkPath(target, "read", SecurityViolationLogger.ViolationType.FILE_READ); } @Test(expected = SecurityException.class) @@ -71,7 +71,7 @@ public void testDeniedFileReadThrows() { Path tmpDir = createTempDir(); // Policy permits nothing buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); - FileAccessInterceptor.checkPath( + FileInterceptor.checkPath( tmpDir.resolve("secret.txt"), "read", SecurityViolationLogger.ViolationType.FILE_READ); } @@ -80,7 +80,7 @@ public void testDeniedFileReadIncrementsFileCounter() { long before = ViolationMetricsReporter.fileCount(); buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); try { - FileAccessInterceptor.checkPath( + FileInterceptor.checkPath( Path.of("/etc/passwd"), "read", SecurityViolationLogger.ViolationType.FILE_READ); } catch (SecurityException ignored) { // expected @@ -88,20 +88,6 @@ public void testDeniedFileReadIncrementsFileCounter() { assertEquals(before + 1, ViolationMetricsReporter.fileCount()); } - // --------------------------------------------------------------------------- - // UNC path tests - // --------------------------------------------------------------------------- - - @Test - public void testUncPathAlwaysBlocked() { - PermittedPath all = new PermittedPath("/", "read", true, PolicyLoader.PolicySource.DEFAULT); - buildEnforcePolicy(List.of(all), List.of(), List.of(), List.of()); - // Even with a broad policy, UNC paths are blocked unconditionally - assertTrue(FileAccessInterceptor.isUncPath("\\\\server\\share\\file")); - assertTrue(FileAccessInterceptor.isUncPath("//server/share/file")); - assertFalse(FileAccessInterceptor.isUncPath("/normal/path")); - } - // --------------------------------------------------------------------------- // Network tests // --------------------------------------------------------------------------- @@ -112,8 +98,7 @@ public void testPermittedEndpointNotBlocked() { new PermittedEndpoint("*:8983", "connect,resolve", null, PolicyLoader.PolicySource.DEFAULT); buildEnforcePolicy(List.of(), List.of(ep), List.of(), List.of()); assertTrue( - NetworkAccessInterceptor.isEndpointPermitted( - SolrSecurityPolicy.getInstance(), "10.0.0.1", 8983)); + SocketChannelInterceptor.isEndpointPermitted(AgentPolicy.getInstance(), "10.0.0.1", 8983)); } @Test(expected = SecurityException.class) @@ -121,7 +106,7 @@ public void testDeniedNetworkConnectThrowsInEnforceMode() throws Exception { buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); java.net.InetSocketAddress addr = new java.net.InetSocketAddress(java.net.InetAddress.getByName("10.0.0.99"), 9999); - NetworkAccessInterceptor.checkConnect(addr); + SocketChannelInterceptor.checkConnect(addr); } @Test @@ -131,7 +116,7 @@ public void testDeniedNetworkIncrementsCounter() throws Exception { java.net.InetSocketAddress addr = new java.net.InetSocketAddress(java.net.InetAddress.getByName("10.0.0.99"), 9999); try { - NetworkAccessInterceptor.checkConnect(addr); + SocketChannelInterceptor.checkConnect(addr); } catch (SecurityException ignored) { // expected } @@ -139,25 +124,26 @@ public void testDeniedNetworkIncrementsCounter() throws Exception { } // --------------------------------------------------------------------------- - // System.exit tests + // System.exit tests — tested via AgentPolicy.isChainThatCanExit() // --------------------------------------------------------------------------- - @Test(expected = SecurityException.class) - public void testUnapprovedExitThrowsInEnforceMode() { + @Test + public void testUnapprovedExitChainDenied() { buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); - ExitInterceptor.checkExit("System.exit(0)"); + assertFalse(AgentPolicy.getInstance().isChainThatCanExit(Set.of(String.class))); } @Test - public void testUnapprovedExitIncrementsCounter() { - long before = ViolationMetricsReporter.exitCount(); - buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); - try { - ExitInterceptor.checkExit("System.exit(0)"); - } catch (SecurityException ignored) { - // expected - } - assertEquals(before + 1, ViolationMetricsReporter.exitCount()); + public void testApprovedExitChainPermitted() { + List exitCallers = + List.of( + new ApprovedCallSite( + SolrAgentIntegrationTest.class.getName(), + ApprovedCallSite.Operation.EXIT, + PolicyLoader.PolicySource.DEFAULT)); + buildEnforcePolicy(List.of(), List.of(), exitCallers, List.of()); + assertTrue( + AgentPolicy.getInstance().isChainThatCanExit(Set.of(SolrAgentIntegrationTest.class))); } // --------------------------------------------------------------------------- diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java index 17c8ebdd5f69..dacedaf6da06 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java @@ -25,7 +25,7 @@ import org.junit.Test; /** - * Tests that symlink-escape attacks are blocked by {@link FileAccessInterceptor}. + * Tests that symlink-escape attacks are blocked by {@link FileInterceptor}. * *

      A symlink inside a permitted directory that points to a target outside permitted directories * must be denied even though the symlink path itself would have matched a permitted rule. The @@ -38,14 +38,14 @@ public class SymlinkEscapeTest extends SolrTestCase { @After public void resetSingleton() { - SolrSecurityPolicy.resetForTesting(); + AgentPolicy.resetForTesting(); } private void resetSingletonSilent() { - SolrSecurityPolicy.resetForTesting(); + AgentPolicy.resetForTesting(); } - private SolrSecurityPolicy buildPolicyPermitting(Path dir) { + private AgentPolicy buildPolicyPermitting(Path dir) { resetSingletonSilent(); // Use the real (symlink-resolved) path so the policy matches after toRealPath() resolution String realDirStr; @@ -56,14 +56,10 @@ private SolrSecurityPolicy buildPolicyPermitting(Path dir) { } PermittedPath allowed = new PermittedPath(realDirStr, "read", true, PolicyLoader.PolicySource.DEFAULT); - SolrSecurityPolicy policy = - new SolrSecurityPolicy( - List.of(allowed), - List.of(), - List.of(), - List.of(), - SolrSecurityPolicy.EnforcementMode.ENFORCE); - SolrSecurityPolicy.initialize(policy); + AgentPolicy policy = + new AgentPolicy( + List.of(allowed), List.of(), List.of(), List.of(), AgentPolicy.EnforcementMode.ENFORCE); + AgentPolicy.initialize(policy); return policy; } @@ -88,8 +84,7 @@ public void testSymlinkInsidePermittedDirIsAllowed() throws Exception { } // Symlink resolves to a path inside the permitted dir — should NOT throw - FileAccessInterceptor.checkPath( - symlink, "read", SecurityViolationLogger.ViolationType.FILE_READ); + FileInterceptor.checkPath(symlink, "read", SecurityViolationLogger.ViolationType.FILE_READ); } // --------------------------------------------------------------------------- @@ -118,8 +113,7 @@ public void testSymlinkEscapeBlockedInEnforceMode() throws Exception { // Accessing via the symlink path should be denied because the REAL path is outside permitted // dir - FileAccessInterceptor.checkPath( - symlink, "read", SecurityViolationLogger.ViolationType.FILE_READ); + FileInterceptor.checkPath(symlink, "read", SecurityViolationLogger.ViolationType.FILE_READ); } @Test @@ -141,8 +135,7 @@ public void testSymlinkEscapeIncrementsFileCounter() throws Exception { long before = ViolationMetricsReporter.fileCount(); try { - FileAccessInterceptor.checkPath( - symlink, "read", SecurityViolationLogger.ViolationType.FILE_READ); + FileInterceptor.checkPath(symlink, "read", SecurityViolationLogger.ViolationType.FILE_READ); } catch (SecurityException ignored) { // expected } @@ -161,7 +154,6 @@ public void testNonExistentPathFallsBackToNormalizedCheck() { // Non-existent file inside the permitted dir — toRealPath() fails → normalized path used Path nonExistent = tmpDir.resolve("does-not-exist.txt"); // Should NOT throw — the normalized path falls within the permitted dir - FileAccessInterceptor.checkPath( - nonExistent, "read", SecurityViolationLogger.ViolationType.FILE_READ); + FileInterceptor.checkPath(nonExistent, "read", SecurityViolationLogger.ViolationType.FILE_READ); } } diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SystemExitInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SystemExitInterceptorTest.java new file mode 100644 index 000000000000..1b86208e0095 --- /dev/null +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SystemExitInterceptorTest.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import org.apache.solr.SolrTestCase; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** Unit tests for {@link SystemExitInterceptor} and {@link RuntimeHaltInterceptor} policy logic. */ +public class SystemExitInterceptorTest extends SolrTestCase { + + @Before + public void snapshotCounters() {} + + @After + public void resetSingleton() { + AgentPolicy.resetForTesting(); + } + + private AgentPolicy initPolicy( + boolean approved, String callerClass, AgentPolicy.EnforcementMode mode) { + AgentPolicy.resetForTesting(); + List exitCallers = + approved + ? List.of( + new ApprovedCallSite( + callerClass, + ApprovedCallSite.Operation.EXIT, + PolicyLoader.PolicySource.DEFAULT)) + : List.of(); + AgentPolicy policy = new AgentPolicy(List.of(), List.of(), exitCallers, List.of(), mode); + AgentPolicy.initialize(policy); + return policy; + } + + @Test + public void testApprovedCallerInChainPermits() { + AgentPolicy policy = + initPolicy( + true, SystemExitInterceptorTest.class.getName(), AgentPolicy.EnforcementMode.ENFORCE); + Collection> chain = Set.of(SystemExitInterceptorTest.class); + assertTrue(policy.isChainThatCanExit(chain)); + } + + @Test + public void testUnapprovedCallerNotInChainDenies() { + AgentPolicy policy = initPolicy(false, "some.other.Class", AgentPolicy.EnforcementMode.ENFORCE); + Collection> chain = Set.of(SystemExitInterceptorTest.class); + assertFalse(policy.isChainThatCanExit(chain)); + } + + @Test + public void testApprovedCallerAnywherInChainPermits() { + AgentPolicy policy = + initPolicy( + true, SystemExitInterceptorTest.class.getName(), AgentPolicy.EnforcementMode.ENFORCE); + // Approved class is deep in the chain — should still grant permission + Collection> chain = + Set.of(String.class, Integer.class, SystemExitInterceptorTest.class); + assertTrue(policy.isChainThatCanExit(chain)); + } + + @Test + public void testWildcardPatternMatchesPackage() { + AgentPolicy.resetForTesting(); + List exitCallers = + List.of( + new ApprovedCallSite( + "org.apache.solr.security.agent.*", + ApprovedCallSite.Operation.EXIT, + PolicyLoader.PolicySource.DEFAULT)); + AgentPolicy policy = + new AgentPolicy( + List.of(), List.of(), exitCallers, List.of(), AgentPolicy.EnforcementMode.ENFORCE); + AgentPolicy.initialize(policy); + + Collection> chain = Set.of(SystemExitInterceptorTest.class); + assertTrue(policy.isChainThatCanExit(chain)); + } + + @Test + public void testEmptyChainDenies() { + AgentPolicy policy = + initPolicy( + true, SystemExitInterceptorTest.class.getName(), AgentPolicy.EnforcementMode.ENFORCE); + assertFalse(policy.isChainThatCanExit(Set.of())); + } +} diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/UncPathRejectionTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/UncPathRejectionTest.java deleted file mode 100644 index 0cfd28333027..000000000000 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/UncPathRejectionTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.solr.security.agent; - -import org.apache.solr.SolrTestCase; -import org.junit.Test; - -/** - * Tests that Windows UNC paths ({@code \\server\share\...} or {@code //server/share/...}) are - * detected and rejected regardless of any policy rule. - * - *

      UNC path blocking is a platform-independent invariant: even on Linux and macOS the detection - * logic must correctly identify and block UNC-style path strings when they appear in an intercepted - * operation (e.g. via a ByteBuddy-intercepted NIO call on Windows or a crafted path string). - * - *

      Note: on POSIX systems Java's {@code Path} normalises {@code //} to {@code /}, so the - * end-to-end enforcement test for forward-slash UNC paths is deferred to Windows CI. These tests - * validate the core detection heuristic which underpins that enforcement. - */ -public class UncPathRejectionTest extends SolrTestCase { - - // --------------------------------------------------------------------------- - // isUncPath detection (unit — platform-independent string check) - // --------------------------------------------------------------------------- - - @Test - public void testBackslashUncDetected() { - assertTrue(FileAccessInterceptor.isUncPath("\\\\server\\share\\file")); - assertTrue(FileAccessInterceptor.isUncPath("\\\\192.168.1.1\\c$\\data")); - assertTrue(FileAccessInterceptor.isUncPath("\\\\?\\Volume{abc}\\file.txt")); - } - - @Test - public void testForwardSlashUncDetected() { - assertTrue(FileAccessInterceptor.isUncPath("//server/share/file")); - assertTrue(FileAccessInterceptor.isUncPath("//192.168.1.1/c$/data/secret")); - } - - @Test - public void testNormalUnixPathsNotDetectedAsUnc() { - assertFalse(FileAccessInterceptor.isUncPath("/normal/path")); - assertFalse(FileAccessInterceptor.isUncPath("/opt/solr/conf")); - assertFalse(FileAccessInterceptor.isUncPath("/tmp/data")); - } - - @Test - public void testWindowsStyleDrivePathNotDetectedAsUnc() { - // Drive-letter paths must NOT be falsely flagged - assertFalse(FileAccessInterceptor.isUncPath("C:\\Windows\\System32")); - assertFalse(FileAccessInterceptor.isUncPath("D:\\data\\backup")); - } - - @Test - public void testRelativePathNotDetectedAsUnc() { - assertFalse(FileAccessInterceptor.isUncPath("relative/path/file.txt")); - assertFalse(FileAccessInterceptor.isUncPath("data/backup")); - } - - @Test - public void testEmptyAndNullSafetyInIsUncPath() { - assertFalse(FileAccessInterceptor.isUncPath("")); - assertFalse(FileAccessInterceptor.isUncPath("/")); - assertFalse(FileAccessInterceptor.isUncPath("\\")); - } -} diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/VirtualThreadCompatibilityTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/VirtualThreadCompatibilityTest.java index 990b7245c2be..52e16767bf77 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/VirtualThreadCompatibilityTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/VirtualThreadCompatibilityTest.java @@ -29,9 +29,8 @@ * Verifies that the Solr security agent enforcement logic is compatible with Java virtual threads * (Project Loom, available since Java 21). * - *

      {@link StackInspector} uses {@link StackWalker} which is virtual-thread–safe by specification - * (JEP 425). These tests exercise the file and network interceptors from virtual threads to - * confirm: + *

      {@link StackWalker} is virtual-thread–safe by specification (JEP 425). These tests exercise + * the file and network interceptors from virtual threads to confirm: * *

        *
      • Permitted operations succeed from a virtual thread context. @@ -44,11 +43,11 @@ public class VirtualThreadCompatibilityTest extends SolrTestCase { @After public void resetSingleton() { - SolrSecurityPolicy.resetForTesting(); + AgentPolicy.resetForTesting(); } private void resetSingletonSilent() { - SolrSecurityPolicy.resetForTesting(); + AgentPolicy.resetForTesting(); } /** Runs {@code task} on a virtual thread and re-throws any exception it produces. */ @@ -93,14 +92,10 @@ public void testPermittedFileAccessFromVirtualThread() throws Exception { } PermittedPath allowed = new PermittedPath(realDirStr, "read", true, PolicyLoader.PolicySource.DEFAULT); - SolrSecurityPolicy policy = - new SolrSecurityPolicy( - List.of(allowed), - List.of(), - List.of(), - List.of(), - SolrSecurityPolicy.EnforcementMode.ENFORCE); - SolrSecurityPolicy.initialize(policy); + AgentPolicy policy = + new AgentPolicy( + List.of(allowed), List.of(), List.of(), List.of(), AgentPolicy.EnforcementMode.ENFORCE); + AgentPolicy.initialize(policy); // Create the file so toRealPath() resolves correctly (no fallback to unresolved path) Path testFile = tmpDir.resolve("test.txt"); @@ -109,23 +104,23 @@ public void testPermittedFileAccessFromVirtualThread() throws Exception { // Must not throw — permitted path from a virtual thread runOnVirtualThread( () -> - FileAccessInterceptor.checkPath( + FileInterceptor.checkPath( testFile, "read", SecurityViolationLogger.ViolationType.FILE_READ)); } @Test public void testDeniedFileAccessFromVirtualThreadIncrementsCounter() throws Exception { resetSingletonSilent(); - SolrSecurityPolicy policy = - new SolrSecurityPolicy( - List.of(), List.of(), List.of(), List.of(), SolrSecurityPolicy.EnforcementMode.ENFORCE); - SolrSecurityPolicy.initialize(policy); + AgentPolicy policy = + new AgentPolicy( + List.of(), List.of(), List.of(), List.of(), AgentPolicy.EnforcementMode.ENFORCE); + AgentPolicy.initialize(policy); long before = ViolationMetricsReporter.fileCount(); try { runOnVirtualThread( () -> - FileAccessInterceptor.checkPath( + FileInterceptor.checkPath( Path.of("/tmp/denied-vt.txt"), "read", SecurityViolationLogger.ViolationType.FILE_READ)); @@ -138,10 +133,10 @@ public void testDeniedFileAccessFromVirtualThreadIncrementsCounter() throws Exce @Test public void testDeniedFileAccessFromVirtualThreadNoStackWalkerException() throws Exception { resetSingletonSilent(); - SolrSecurityPolicy policy = - new SolrSecurityPolicy( - List.of(), List.of(), List.of(), List.of(), SolrSecurityPolicy.EnforcementMode.WARN); - SolrSecurityPolicy.initialize(policy); + AgentPolicy policy = + new AgentPolicy( + List.of(), List.of(), List.of(), List.of(), AgentPolicy.EnforcementMode.WARN); + AgentPolicy.initialize(policy); // In warn mode, no SecurityException is thrown, but we must verify no NPE/CCE from StackWalker AtomicReference unexpected = new AtomicReference<>(); @@ -150,7 +145,7 @@ public void testDeniedFileAccessFromVirtualThreadNoStackWalkerException() throws .start( () -> { try { - FileAccessInterceptor.checkPath( + FileInterceptor.checkPath( Path.of("/tmp/vt-check.txt"), "read", SecurityViolationLogger.ViolationType.FILE_READ); @@ -175,29 +170,25 @@ public void testPermittedNetworkFromVirtualThread() throws Exception { resetSingletonSilent(); PermittedEndpoint ep = new PermittedEndpoint("*:8983", "connect,resolve", null, PolicyLoader.PolicySource.DEFAULT); - SolrSecurityPolicy policy = - new SolrSecurityPolicy( - List.of(), - List.of(ep), - List.of(), - List.of(), - SolrSecurityPolicy.EnforcementMode.ENFORCE); - SolrSecurityPolicy.initialize(policy); + AgentPolicy policy = + new AgentPolicy( + List.of(), List.of(ep), List.of(), List.of(), AgentPolicy.EnforcementMode.ENFORCE); + AgentPolicy.initialize(policy); runOnVirtualThread( () -> assertTrue( - NetworkAccessInterceptor.isEndpointPermitted( - SolrSecurityPolicy.getInstance(), "10.0.0.1", 8983))); + SocketChannelInterceptor.isEndpointPermitted( + AgentPolicy.getInstance(), "10.0.0.1", 8983))); } @Test public void testDeniedNetworkFromVirtualThreadIncrementsCounter() throws Exception { resetSingletonSilent(); - SolrSecurityPolicy policy = - new SolrSecurityPolicy( - List.of(), List.of(), List.of(), List.of(), SolrSecurityPolicy.EnforcementMode.ENFORCE); - SolrSecurityPolicy.initialize(policy); + AgentPolicy policy = + new AgentPolicy( + List.of(), List.of(), List.of(), List.of(), AgentPolicy.EnforcementMode.ENFORCE); + AgentPolicy.initialize(policy); long before = ViolationMetricsReporter.networkCount(); try { @@ -205,7 +196,7 @@ public void testDeniedNetworkFromVirtualThreadIncrementsCounter() throws Excepti () -> { InetSocketAddress addr = new InetSocketAddress(InetAddress.getByName("10.0.0.99"), 9999); - NetworkAccessInterceptor.checkConnect(addr); + SocketChannelInterceptor.checkConnect(addr); }); } catch (SecurityException ignored) { // expected diff --git a/solr/bin/solr b/solr/bin/solr index c9c2daa85fae..f62eb0c9c6d8 100755 --- a/solr/bin/solr +++ b/solr/bin/solr @@ -1168,19 +1168,13 @@ fi # Security agent: detect agent JAR and add -javaagent: if present and not skipped. # Set SOLR_SECURITY_AGENT_SKIP=true to disable the agent (for troubleshooting only). +# Agent reads its configuration (SOLR_SECURITY_AGENT_MODE, SOLR_SECURITY_AGENT_EXTRA_POLICY, +# and policy file variables like SOLR_HOME) directly from env vars — no -D flags needed. AGENT_SM_OPTS=() if [ "${SOLR_SECURITY_AGENT_SKIP:-false}" != "true" ]; then AGENT_JAR=$(ls "${SOLR_SERVER_DIR}/lib/ext/solr-agent-sm-"*.jar 2>/dev/null | head -1) if [ -n "$AGENT_JAR" ]; then AGENT_SM_OPTS+=("-javaagent:${AGENT_JAR}") - # Convert env vars to JVM system properties so the agent can read them via System.getProperty() - # (the agent JAR has no dependency on solr:core and cannot use EnvUtils). - if [ -n "${SOLR_SECURITY_AGENT_MODE:-}" ]; then - AGENT_SM_OPTS+=("-Dsolr.security.agent.mode=${SOLR_SECURITY_AGENT_MODE}") - fi - if [ -n "${SOLR_SECURITY_AGENT_EXTRA_POLICY:-}" ]; then - AGENT_SM_OPTS+=("-Dsolr.security.agent.extra.policy=${SOLR_SECURITY_AGENT_EXTRA_POLICY}") - fi fi fi diff --git a/solr/bin/solr.cmd b/solr/bin/solr.cmd index cd0511adc2d1..58d39c8c4452 100755 --- a/solr/bin/solr.cmd +++ b/solr/bin/solr.cmd @@ -1023,13 +1023,13 @@ IF "%GC_TUNE%"=="" ( REM Security agent: detect agent JAR and add -javaagent: if present and not skipped. REM Set SOLR_SECURITY_AGENT_SKIP=true to disable the agent (for troubleshooting only). +REM Agent reads its configuration (SOLR_SECURITY_AGENT_MODE, SOLR_SECURITY_AGENT_EXTRA_POLICY, +REM and policy file variables like SOLR_HOME) directly from env vars -- no -D flags needed. set AGENT_SM_OPTS= IF NOT "%SOLR_SECURITY_AGENT_SKIP%"=="true" ( IF EXIST "%SOLR_SERVER_DIR%\lib\ext\solr-agent-sm-*.jar" ( FOR %%F IN ("%SOLR_SERVER_DIR%\lib\ext\solr-agent-sm-*.jar") DO SET "AGENT_JAR=%%F" SET "AGENT_SM_OPTS=-javaagent:!AGENT_JAR!" - IF DEFINED SOLR_SECURITY_AGENT_MODE SET "AGENT_SM_OPTS=!AGENT_SM_OPTS! -Dsolr.security.agent.mode=!SOLR_SECURITY_AGENT_MODE!" - IF DEFINED SOLR_SECURITY_AGENT_EXTRA_POLICY SET "AGENT_SM_OPTS=!AGENT_SM_OPTS! -Dsolr.security.agent.extra.policy=!SOLR_SECURITY_AGENT_EXTRA_POLICY!" ) ) diff --git a/solr/packaging/test/test_security_agent.bats b/solr/packaging/test/test_security_agent.bats index 92236a0448d8..1a741ebd375a 100644 --- a/solr/packaging/test/test_security_agent.bats +++ b/solr/packaging/test/test_security_agent.bats @@ -105,3 +105,37 @@ EOF with_extra=$(grep -oE "permitted paths=[0-9]+" "$(console_log)" | grep -oE "[0-9]+" | head -1) [ "${with_extra:-0}" -gt 0 ] } + +@test "enforce mode blocks unauthorized file access with SecurityException" { + local agent_jar + agent_jar=$(ls "${SOLR_TIP}/server/lib/ext/solr-agent-sm-"*.jar) + + # Tiny program that reads /etc/hosts — outside every path the default policy permits. + cat > "${BATS_TEST_TMPDIR}/FileViolation.java" <<'EOF' +public class FileViolation { + public static void main(String[] args) throws Exception { + java.nio.file.Files.readAllBytes(java.nio.file.Path.of("/etc/hosts")); + System.out.println("read succeeded -- agent did NOT block"); + } +} +EOF + + javac -d "${BATS_TEST_TMPDIR}" "${BATS_TEST_TMPDIR}/FileViolation.java" + + # Run standalone under the agent in enforce mode — no Solr startup needed. + # Provide the sysprops that bin/solr would normally pass so policy variables resolve. + run java \ + -javaagent:"${agent_jar}" \ + -Dsolr.security.agent.mode=enforce \ + -Dsolr.install.dir="${SOLR_TIP}" \ + -Dsolr.solr.home="${SOLR_TIP}/server/solr" \ + -Dsolr.logs.dir="${BATS_TEST_TMPDIR}" \ + -Dsolr.port.listen=8983 \ + -cp "${BATS_TEST_TMPDIR}" \ + FileViolation + + # SecurityException propagates uncaught → non-zero exit with stack trace in output. + assert_failure + assert_output --partial "SecurityException" + refute_output --partial "read succeeded" +} diff --git a/solr/server/etc/agent-security.policy b/solr/server/etc/agent-security.policy index 828b50617b8c..deba7a11033c 100644 --- a/solr/server/etc/agent-security.policy +++ b/solr/server/etc/agent-security.policy @@ -23,8 +23,8 @@ // SOLR_SECURITY_AGENT_EXTRA_POLICY / -Dsolr.security.agent.extra.policy). // // Syntax: JDK-style .policy files with Solr variable substitution. -// Supported variables: ${solr.home}, ${solr.data.dir}, ${solr.log.dir}, ${solr.install.dir}, -// ${java.io.tmpdir}, ${java.home}, ${solr.port}, ${solr.zk.port} +// Supported variables: ${solr.solr.home}, ${solr.data.home}, ${solr.logs.dir}, ${solr.install.dir}, +// ${java.io.tmpdir}, ${java.home}, ${solr.port.listen}, ${solr.zk.port} // // For documentation see the Solr Reference Guide: Security Agent. @@ -35,14 +35,17 @@ grant { // --- File system access --- + // Solr installation directory: read-only (JARs, modules, Jetty config, etc.) + permission java.io.FilePermission "${solr.install.dir}/-", "read"; + // Solr home: read-only (config files, schema, solrconfig.xml, etc.) - permission java.io.FilePermission "${solr.home}/-", "read"; + permission java.io.FilePermission "${solr.solr.home}/-", "read"; // Solr data and index directories: full read/write/delete - permission java.io.FilePermission "${solr.data.dir}/-", "read,write,delete"; + permission java.io.FilePermission "${solr.data.home}/-", "read,write,delete"; // Log directory: write access for log files - permission java.io.FilePermission "${solr.log.dir}/-", "read,write,delete"; + permission java.io.FilePermission "${solr.logs.dir}/-", "read,write,delete"; // Temporary files: full access (sort buffers, merged segments, etc.) permission java.io.FilePermission "${java.io.tmpdir}/-", "read,write,delete"; @@ -60,8 +63,8 @@ grant { // Intra-cluster connectivity — Decision 9 (port-wildcard approach): // Any host on the Solr HTTP port is permitted, covering all current and future cluster nodes // without requiring a static node list at startup. Similarly for the ZooKeeper port. - permission java.net.SocketPermission "*:${solr.port}", "connect,resolve"; - permission java.net.SocketPermission "*:${solr.zk.port}", "connect,resolve"; + permission java.net.SocketPermission "*:${solr.port.listen}", "connect,resolve"; + permission java.net.SocketPermission "*:${solr.zk.port}", "connect,resolve"; }; From 25519b66c2ee32cecd8b6ac9566b724eacd1563b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 15:18:29 +0200 Subject: [PATCH 05/65] SOLR-17767: Pre-compile BATS violation programs; add tests 5-7 for exit/network/exec; remove redundant VirtualThreadCompatibilityTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add testPrograms sourceSet in agent-sm compiling FileViolation, ExitViolation, NetworkViolation, ExecViolation into a standalone JAR (no Solr/ByteBuddy deps). - Wire AGENT_TEST_PROGRAMS_JAR into integrationTests task; BATS test #4 drops the inline javac+heredoc in favour of the pre-compiled JAR. - Add BATS tests #5 (System.exit), #6 (SocketChannel.connect to RFC 5737 TEST-NET-1), #7 (ProcessBuilder exec) — all verify SecurityException in enforce mode. - Delete VirtualThreadCompatibilityTest: StackWalker virtual-thread compatibility is guaranteed by JDK spec and already exercised by other unit tests. - Fix rat-sources.gradle: exclude src/test-programs/** for :solr:agent-sm. --- gradle/validation/rat-sources.gradle | 5 + solr/agent-sm/build.gradle | 18 ++ .../solr/security/agent/AgentPolicy.java | 3 +- .../src/test-programs/java/ExecViolation.java | 29 +++ .../src/test-programs/java/ExitViolation.java | 29 +++ .../src/test-programs/java/FileViolation.java | 27 +++ .../test-programs/java/NetworkViolation.java | 33 +++ .../agent/VirtualThreadCompatibilityTest.java | 206 ------------------ solr/packaging/build.gradle | 2 + solr/packaging/test/test_security_agent.bats | 78 +++++-- 10 files changed, 207 insertions(+), 223 deletions(-) create mode 100644 solr/agent-sm/src/test-programs/java/ExecViolation.java create mode 100644 solr/agent-sm/src/test-programs/java/ExitViolation.java create mode 100644 solr/agent-sm/src/test-programs/java/FileViolation.java create mode 100644 solr/agent-sm/src/test-programs/java/NetworkViolation.java delete mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/VirtualThreadCompatibilityTest.java diff --git a/gradle/validation/rat-sources.gradle b/gradle/validation/rat-sources.gradle index beb3efa01802..92c573c29f6f 100644 --- a/gradle/validation/rat-sources.gradle +++ b/gradle/validation/rat-sources.gradle @@ -151,6 +151,11 @@ allprojects { exclude "src/test-files/**/*-snippet-*.xml" break + case ":solr:agent-sm": + // Test-programs are tiny standalone programs for BATS tests, not shipped artifacts. + exclude "src/test-programs/**" + break + case ":solr:server": exclude "**/*.xml" exclude "**/*.sh" diff --git a/solr/agent-sm/build.gradle b/solr/agent-sm/build.gradle index 2421b5168b5a..57e96147de8f 100644 --- a/solr/agent-sm/build.gradle +++ b/solr/agent-sm/build.gradle @@ -95,3 +95,21 @@ configurations.jarValidation { } assemble.dependsOn copyAgentJar + +// --------------------------------------------------------------------------- +// Test programs — standalone Java programs compiled for BATS integration tests. +// These have no Solr/ByteBuddy dependencies; pure JDK APIs only. +// --------------------------------------------------------------------------- +sourceSets { + testPrograms { + java.srcDirs = ['src/test-programs/java'] + } +} + +task testProgramsJar(type: Jar) { + description = 'JAR of standalone violation programs used by BATS integration tests' + group = 'build' + archiveClassifier = 'test-programs' + from sourceSets.testPrograms.output +} + diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java index 92971ef246f1..dd2085647c2c 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java @@ -238,8 +238,7 @@ public boolean isExecApproved(String className) { /** * Returns {@code true} if any class in the call chain is approved to call {@code System.exit()} - * or {@code Runtime.halt()}. Mirrors {@code AgentPolicy.isChainThatCanExit()} in the OpenSearch - * reference implementation: any approved class anywhere in the chain grants permission. + * or {@code Runtime.halt()}. Any approved class anywhere in the chain grants permission. * *

        Class names are matched using {@link String#matches} (full regex), so the approved-caller * list supports wildcard patterns such as {@code "org\\.apache\\.solr\\..*"}. diff --git a/solr/agent-sm/src/test-programs/java/ExecViolation.java b/solr/agent-sm/src/test-programs/java/ExecViolation.java new file mode 100644 index 000000000000..04f786c71703 --- /dev/null +++ b/solr/agent-sm/src/test-programs/java/ExecViolation.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * BATS test program: spawns a child process via ProcessBuilder. The default policy grants no + * exec permission, so this should be blocked by ProcessExecInterceptor in enforce mode. + * Expected: SecurityException thrown, process exits non-zero. + */ +public class ExecViolation { + public static void main(String[] args) throws Exception { + System.out.println("attempting process exec"); + new ProcessBuilder("echo", "blocked").start().waitFor(); + System.out.println("exec succeeded -- agent did NOT block"); + } +} diff --git a/solr/agent-sm/src/test-programs/java/ExitViolation.java b/solr/agent-sm/src/test-programs/java/ExitViolation.java new file mode 100644 index 000000000000..5a430a93ea5f --- /dev/null +++ b/solr/agent-sm/src/test-programs/java/ExitViolation.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * BATS test program: calls System.exit(0). The default policy grants no exitVM permission + * to anonymous codeBase, so this should be blocked by SystemExitInterceptor in enforce mode. + * Expected: SecurityException thrown, process exits non-zero. + */ +public class ExitViolation { + public static void main(String[] args) { + System.out.println("attempting System.exit"); + System.exit(0); + System.out.println("exit succeeded -- agent did NOT block"); + } +} diff --git a/solr/agent-sm/src/test-programs/java/FileViolation.java b/solr/agent-sm/src/test-programs/java/FileViolation.java new file mode 100644 index 000000000000..e8a6526f7bfb --- /dev/null +++ b/solr/agent-sm/src/test-programs/java/FileViolation.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * BATS test program: reads /etc/hosts, which is outside every path the default policy permits. + * Expected: SecurityException thrown by FileInterceptor in enforce mode. + */ +public class FileViolation { + public static void main(String[] args) throws Exception { + java.nio.file.Files.readAllBytes(java.nio.file.Path.of("/etc/hosts")); + System.out.println("read succeeded -- agent did NOT block"); + } +} diff --git a/solr/agent-sm/src/test-programs/java/NetworkViolation.java b/solr/agent-sm/src/test-programs/java/NetworkViolation.java new file mode 100644 index 000000000000..45066b6c9d68 --- /dev/null +++ b/solr/agent-sm/src/test-programs/java/NetworkViolation.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * BATS test program: opens a SocketChannel to 192.0.2.1:443 (TEST-NET-1, RFC 5737 — guaranteed + * non-routable). The address is not in the trusted-hosts set and port 443 is not in the default + * policy. SocketChannelInterceptor fires before any TCP I/O, so the test completes instantly. + * Expected: SecurityException thrown by SocketChannelInterceptor in enforce mode. + */ +public class NetworkViolation { + public static void main(String[] args) throws Exception { + System.out.println("attempting socket connect"); + var addr = new java.net.InetSocketAddress("192.0.2.1", 443); + try (var ch = java.nio.channels.SocketChannel.open()) { + ch.connect(addr); + } + System.out.println("connect succeeded -- agent did NOT block"); + } +} diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/VirtualThreadCompatibilityTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/VirtualThreadCompatibilityTest.java deleted file mode 100644 index 52e16767bf77..000000000000 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/VirtualThreadCompatibilityTest.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.solr.security.agent; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.nio.file.Path; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import org.apache.solr.SolrTestCase; -import org.junit.After; -import org.junit.Test; - -/** - * Verifies that the Solr security agent enforcement logic is compatible with Java virtual threads - * (Project Loom, available since Java 21). - * - *

        {@link StackWalker} is virtual-thread–safe by specification (JEP 425). These tests exercise - * the file and network interceptors from virtual threads to confirm: - * - *

          - *
        • Permitted operations succeed from a virtual thread context. - *
        • Denied operations throw {@link SecurityException} and increment counters correctly. - *
        • No {@link NullPointerException} or {@link ClassCastException} arises from stack walking on - * virtual-thread frames. - *
        - */ -public class VirtualThreadCompatibilityTest extends SolrTestCase { - - @After - public void resetSingleton() { - AgentPolicy.resetForTesting(); - } - - private void resetSingletonSilent() { - AgentPolicy.resetForTesting(); - } - - /** Runs {@code task} on a virtual thread and re-throws any exception it produces. */ - private static void runOnVirtualThread(RunnableWithException task) throws Exception { - AtomicReference caught = new AtomicReference<>(); - Thread vt = - Thread.ofVirtual() - .start( - () -> { - try { - task.run(); - } catch (Throwable t) { - caught.set(t); - } - }); - vt.join(); - Throwable t = caught.get(); - if (t instanceof Exception e) throw e; - if (t instanceof Error e) throw e; - if (t != null) throw new RuntimeException(t); - } - - @FunctionalInterface - interface RunnableWithException { - void run() throws Exception; - } - - // --------------------------------------------------------------------------- - // File access from virtual threads - // --------------------------------------------------------------------------- - - @Test - public void testPermittedFileAccessFromVirtualThread() throws Exception { - Path tmpDir = createTempDir(); - resetSingletonSilent(); - // Use the real (symlink-resolved) path so the policy matches after toRealPath() resolution - String realDirStr; - try { - realDirStr = tmpDir.toRealPath().toString(); - } catch (java.io.IOException e) { - realDirStr = tmpDir.toAbsolutePath().toString(); - } - PermittedPath allowed = - new PermittedPath(realDirStr, "read", true, PolicyLoader.PolicySource.DEFAULT); - AgentPolicy policy = - new AgentPolicy( - List.of(allowed), List.of(), List.of(), List.of(), AgentPolicy.EnforcementMode.ENFORCE); - AgentPolicy.initialize(policy); - - // Create the file so toRealPath() resolves correctly (no fallback to unresolved path) - Path testFile = tmpDir.resolve("test.txt"); - java.nio.file.Files.writeString(testFile, "data"); - - // Must not throw — permitted path from a virtual thread - runOnVirtualThread( - () -> - FileInterceptor.checkPath( - testFile, "read", SecurityViolationLogger.ViolationType.FILE_READ)); - } - - @Test - public void testDeniedFileAccessFromVirtualThreadIncrementsCounter() throws Exception { - resetSingletonSilent(); - AgentPolicy policy = - new AgentPolicy( - List.of(), List.of(), List.of(), List.of(), AgentPolicy.EnforcementMode.ENFORCE); - AgentPolicy.initialize(policy); - - long before = ViolationMetricsReporter.fileCount(); - try { - runOnVirtualThread( - () -> - FileInterceptor.checkPath( - Path.of("/tmp/denied-vt.txt"), - "read", - SecurityViolationLogger.ViolationType.FILE_READ)); - } catch (SecurityException ignored) { - // expected - } - assertEquals(before + 1, ViolationMetricsReporter.fileCount()); - } - - @Test - public void testDeniedFileAccessFromVirtualThreadNoStackWalkerException() throws Exception { - resetSingletonSilent(); - AgentPolicy policy = - new AgentPolicy( - List.of(), List.of(), List.of(), List.of(), AgentPolicy.EnforcementMode.WARN); - AgentPolicy.initialize(policy); - - // In warn mode, no SecurityException is thrown, but we must verify no NPE/CCE from StackWalker - AtomicReference unexpected = new AtomicReference<>(); - Thread vt = - Thread.ofVirtual() - .start( - () -> { - try { - FileInterceptor.checkPath( - Path.of("/tmp/vt-check.txt"), - "read", - SecurityViolationLogger.ViolationType.FILE_READ); - } catch (SecurityException ignored) { - // Expected in enforce mode - } catch (Throwable t) { - unexpected.set(t); - } - }); - vt.join(); - assertNull( - "Unexpected exception from virtual-thread stack walk: " + unexpected.get(), - unexpected.get()); - } - - // --------------------------------------------------------------------------- - // Network access from virtual threads - // --------------------------------------------------------------------------- - - @Test - public void testPermittedNetworkFromVirtualThread() throws Exception { - resetSingletonSilent(); - PermittedEndpoint ep = - new PermittedEndpoint("*:8983", "connect,resolve", null, PolicyLoader.PolicySource.DEFAULT); - AgentPolicy policy = - new AgentPolicy( - List.of(), List.of(ep), List.of(), List.of(), AgentPolicy.EnforcementMode.ENFORCE); - AgentPolicy.initialize(policy); - - runOnVirtualThread( - () -> - assertTrue( - SocketChannelInterceptor.isEndpointPermitted( - AgentPolicy.getInstance(), "10.0.0.1", 8983))); - } - - @Test - public void testDeniedNetworkFromVirtualThreadIncrementsCounter() throws Exception { - resetSingletonSilent(); - AgentPolicy policy = - new AgentPolicy( - List.of(), List.of(), List.of(), List.of(), AgentPolicy.EnforcementMode.ENFORCE); - AgentPolicy.initialize(policy); - - long before = ViolationMetricsReporter.networkCount(); - try { - runOnVirtualThread( - () -> { - InetSocketAddress addr = - new InetSocketAddress(InetAddress.getByName("10.0.0.99"), 9999); - SocketChannelInterceptor.checkConnect(addr); - }); - } catch (SecurityException ignored) { - // expected - } - assertEquals(before + 1, ViolationMetricsReporter.networkCount()); - } -} diff --git a/solr/packaging/build.gradle b/solr/packaging/build.gradle index 7320f2ebe2b2..b27e23d6b19d 100644 --- a/solr/packaging/build.gradle +++ b/solr/packaging/build.gradle @@ -266,6 +266,7 @@ task downloadBats(type: NpmTask) { task integrationTests(type: BatsTask) { dependsOn installFullDist dependsOn downloadBats + dependsOn ':solr:agent-sm:testProgramsJar' def integrationTestOutput = "$buildDir/test-output" def solrHome = "$integrationTestOutput/solr-home" @@ -298,6 +299,7 @@ task integrationTests(type: BatsTask) { } environment SOLR_TIP: distDir.toString() + environment AGENT_TEST_PROGRAMS_JAR: project(':solr:agent-sm').tasks.named('testProgramsJar').get().archiveFile.get().asFile environment SOLR_HOME: solrHome environment SOLR_PID_DIR: solrHome environment SOLR_PORT_LISTEN: solrPort diff --git a/solr/packaging/test/test_security_agent.bats b/solr/packaging/test/test_security_agent.bats index 1a741ebd375a..c5ddb69b47ce 100644 --- a/solr/packaging/test/test_security_agent.bats +++ b/solr/packaging/test/test_security_agent.bats @@ -110,20 +110,28 @@ EOF local agent_jar agent_jar=$(ls "${SOLR_TIP}/server/lib/ext/solr-agent-sm-"*.jar) - # Tiny program that reads /etc/hosts — outside every path the default policy permits. - cat > "${BATS_TEST_TMPDIR}/FileViolation.java" <<'EOF' -public class FileViolation { - public static void main(String[] args) throws Exception { - java.nio.file.Files.readAllBytes(java.nio.file.Path.of("/etc/hosts")); - System.out.println("read succeeded -- agent did NOT block"); - } + # FileViolation reads /etc/hosts — outside every path the default policy permits. + # The class is pre-compiled into AGENT_TEST_PROGRAMS_JAR by the Gradle build. + run java \ + -javaagent:"${agent_jar}" \ + -Dsolr.security.agent.mode=enforce \ + -Dsolr.install.dir="${SOLR_TIP}" \ + -Dsolr.solr.home="${SOLR_TIP}/server/solr" \ + -Dsolr.logs.dir="${BATS_TEST_TMPDIR}" \ + -Dsolr.port.listen=8983 \ + -cp "${AGENT_TEST_PROGRAMS_JAR}" \ + FileViolation + + assert_failure + assert_output --partial "SecurityException" + refute_output --partial "read succeeded" } -EOF - javac -d "${BATS_TEST_TMPDIR}" "${BATS_TEST_TMPDIR}/FileViolation.java" +@test "enforce mode blocks System.exit with SecurityException" { + local agent_jar + agent_jar=$(ls "${SOLR_TIP}/server/lib/ext/solr-agent-sm-"*.jar) - # Run standalone under the agent in enforce mode — no Solr startup needed. - # Provide the sysprops that bin/solr would normally pass so policy variables resolve. + # ExitViolation calls System.exit(0) — no exitVM grant in the default policy. run java \ -javaagent:"${agent_jar}" \ -Dsolr.security.agent.mode=enforce \ @@ -131,11 +139,51 @@ EOF -Dsolr.solr.home="${SOLR_TIP}/server/solr" \ -Dsolr.logs.dir="${BATS_TEST_TMPDIR}" \ -Dsolr.port.listen=8983 \ - -cp "${BATS_TEST_TMPDIR}" \ - FileViolation + -cp "${AGENT_TEST_PROGRAMS_JAR}" \ + ExitViolation - # SecurityException propagates uncaught → non-zero exit with stack trace in output. assert_failure assert_output --partial "SecurityException" - refute_output --partial "read succeeded" + refute_output --partial "exit succeeded" +} + +@test "enforce mode blocks unauthorized outbound connection with SecurityException" { + local agent_jar + agent_jar=$(ls "${SOLR_TIP}/server/lib/ext/solr-agent-sm-"*.jar) + + # NetworkViolation opens a SocketChannel to 192.0.2.1:443 (TEST-NET-1, RFC 5737 — + # guaranteed non-routable). The interceptor fires before any TCP I/O, so this is instant. + run java \ + -javaagent:"${agent_jar}" \ + -Dsolr.security.agent.mode=enforce \ + -Dsolr.install.dir="${SOLR_TIP}" \ + -Dsolr.solr.home="${SOLR_TIP}/server/solr" \ + -Dsolr.logs.dir="${BATS_TEST_TMPDIR}" \ + -Dsolr.port.listen=8983 \ + -cp "${AGENT_TEST_PROGRAMS_JAR}" \ + NetworkViolation + + assert_failure + assert_output --partial "SecurityException" + refute_output --partial "connect succeeded" +} + +@test "enforce mode blocks process exec with SecurityException" { + local agent_jar + agent_jar=$(ls "${SOLR_TIP}/server/lib/ext/solr-agent-sm-"*.jar) + + # ExecViolation spawns a child process via ProcessBuilder — no exec grant in default policy. + run java \ + -javaagent:"${agent_jar}" \ + -Dsolr.security.agent.mode=enforce \ + -Dsolr.install.dir="${SOLR_TIP}" \ + -Dsolr.solr.home="${SOLR_TIP}/server/solr" \ + -Dsolr.logs.dir="${BATS_TEST_TMPDIR}" \ + -Dsolr.port.listen=8983 \ + -cp "${AGENT_TEST_PROGRAMS_JAR}" \ + ExecViolation + + assert_failure + assert_output --partial "SecurityException" + refute_output --partial "exec succeeded" } From 3dc31c03a63ecd63e55fa897384a93c65c3ade20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 15:23:42 +0200 Subject: [PATCH 06/65] SOLR-17767: Use System.exit(123) sentinel in ExitViolation; assert status != 123 in BATS System.exit(0) is unreachable after a successful exit, making refute_output vacuous and masking a broken agent (exit code 0 would trigger assert_failure for the wrong reason). Using exit code 123 as a sentinel lets the BATS test explicitly verify that the interceptor fired (SecurityException in output, status != 123). --- .../src/test-programs/java/ExitViolation.java | 18 ++++++++++-------- solr/packaging/test/test_security_agent.bats | 6 ++++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/solr/agent-sm/src/test-programs/java/ExitViolation.java b/solr/agent-sm/src/test-programs/java/ExitViolation.java index 5a430a93ea5f..1350ff40bea7 100644 --- a/solr/agent-sm/src/test-programs/java/ExitViolation.java +++ b/solr/agent-sm/src/test-programs/java/ExitViolation.java @@ -16,14 +16,16 @@ */ /** - * BATS test program: calls System.exit(0). The default policy grants no exitVM permission - * to anonymous codeBase, so this should be blocked by SystemExitInterceptor in enforce mode. - * Expected: SecurityException thrown, process exits non-zero. + * BATS test program: calls System.exit(123). The default policy grants no exitVM permission to + * anonymous codeBase, so this should be blocked by SystemExitInterceptor in enforce mode. + * + *

        Exit code 123 is used as a sentinel: if the process exits with that code the agent did NOT + * block the call. The BATS test asserts both a SecurityException in the output and that {@code + * $status != 123}. */ public class ExitViolation { - public static void main(String[] args) { - System.out.println("attempting System.exit"); - System.exit(0); - System.out.println("exit succeeded -- agent did NOT block"); - } + public static void main(String[] args) { + System.out.println("attempting System.exit"); + System.exit(123); + } } diff --git a/solr/packaging/test/test_security_agent.bats b/solr/packaging/test/test_security_agent.bats index c5ddb69b47ce..65062b58ae7e 100644 --- a/solr/packaging/test/test_security_agent.bats +++ b/solr/packaging/test/test_security_agent.bats @@ -131,7 +131,9 @@ EOF local agent_jar agent_jar=$(ls "${SOLR_TIP}/server/lib/ext/solr-agent-sm-"*.jar) - # ExitViolation calls System.exit(0) — no exitVM grant in the default policy. + # ExitViolation calls System.exit(123) — no exitVM grant in the default policy. + # Exit code 123 is a sentinel: if the process exits with that exact code, the agent + # did NOT block the call and System.exit(123) ran unimpeded. run java \ -javaagent:"${agent_jar}" \ -Dsolr.security.agent.mode=enforce \ @@ -144,7 +146,7 @@ EOF assert_failure assert_output --partial "SecurityException" - refute_output --partial "exit succeeded" + [ "$status" -ne 123 ] # status 123 means System.exit(123) ran — agent did NOT block } @test "enforce mode blocks unauthorized outbound connection with SecurityException" { From 8297df06793d41dd17a973400517db1783784cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 15:35:58 +0200 Subject: [PATCH 07/65] Fix exit bats test --- .../src/test-programs/java/ExecViolation.java | 16 ++++++++-------- .../src/test-programs/java/FileViolation.java | 8 ++++---- .../src/test-programs/java/NetworkViolation.java | 14 +++++++------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/solr/agent-sm/src/test-programs/java/ExecViolation.java b/solr/agent-sm/src/test-programs/java/ExecViolation.java index 04f786c71703..6851c0ac7f97 100644 --- a/solr/agent-sm/src/test-programs/java/ExecViolation.java +++ b/solr/agent-sm/src/test-programs/java/ExecViolation.java @@ -16,14 +16,14 @@ */ /** - * BATS test program: spawns a child process via ProcessBuilder. The default policy grants no - * exec permission, so this should be blocked by ProcessExecInterceptor in enforce mode. - * Expected: SecurityException thrown, process exits non-zero. + * BATS test program: spawns a child process via ProcessBuilder. The default policy grants no exec + * permission, so this should be blocked by ProcessExecInterceptor in enforce mode. Expected: + * SecurityException thrown, process exits non-zero. */ public class ExecViolation { - public static void main(String[] args) throws Exception { - System.out.println("attempting process exec"); - new ProcessBuilder("echo", "blocked").start().waitFor(); - System.out.println("exec succeeded -- agent did NOT block"); - } + public static void main(String[] args) throws Exception { + System.out.println("attempting process exec"); + new ProcessBuilder("echo", "blocked").start().waitFor(); + System.out.println("exec succeeded -- agent did NOT block"); + } } diff --git a/solr/agent-sm/src/test-programs/java/FileViolation.java b/solr/agent-sm/src/test-programs/java/FileViolation.java index e8a6526f7bfb..db6aaa3d7a44 100644 --- a/solr/agent-sm/src/test-programs/java/FileViolation.java +++ b/solr/agent-sm/src/test-programs/java/FileViolation.java @@ -20,8 +20,8 @@ * Expected: SecurityException thrown by FileInterceptor in enforce mode. */ public class FileViolation { - public static void main(String[] args) throws Exception { - java.nio.file.Files.readAllBytes(java.nio.file.Path.of("/etc/hosts")); - System.out.println("read succeeded -- agent did NOT block"); - } + public static void main(String[] args) throws Exception { + java.nio.file.Files.readAllBytes(java.nio.file.Path.of("/etc/hosts")); + System.out.println("read succeeded -- agent did NOT block"); + } } diff --git a/solr/agent-sm/src/test-programs/java/NetworkViolation.java b/solr/agent-sm/src/test-programs/java/NetworkViolation.java index 45066b6c9d68..1f0983a53088 100644 --- a/solr/agent-sm/src/test-programs/java/NetworkViolation.java +++ b/solr/agent-sm/src/test-programs/java/NetworkViolation.java @@ -22,12 +22,12 @@ * Expected: SecurityException thrown by SocketChannelInterceptor in enforce mode. */ public class NetworkViolation { - public static void main(String[] args) throws Exception { - System.out.println("attempting socket connect"); - var addr = new java.net.InetSocketAddress("192.0.2.1", 443); - try (var ch = java.nio.channels.SocketChannel.open()) { - ch.connect(addr); - } - System.out.println("connect succeeded -- agent did NOT block"); + public static void main(String[] args) throws Exception { + System.out.println("attempting socket connect"); + var addr = new java.net.InetSocketAddress("192.0.2.1", 443); + try (var ch = java.nio.channels.SocketChannel.open()) { + ch.connect(addr); } + System.out.println("connect succeeded -- agent did NOT block"); + } } From 69499caac3c36a1aa98960ef5dd918af333c343a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 15:45:26 +0200 Subject: [PATCH 08/65] Lockfiles --- solr/agent-sm/gradle.lockfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/solr/agent-sm/gradle.lockfile b/solr/agent-sm/gradle.lockfile index 4dbbe046cd7d..1db2dc26818e 100644 --- a/solr/agent-sm/gradle.lockfile +++ b/solr/agent-sm/gradle.lockfile @@ -34,7 +34,8 @@ com.tdunning:t-digest:3.3=jarValidation,permitTestUnusedDeclared,testRuntimeClas commons-cli:commons-cli:1.11.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy commons-codec:commons-codec:1.21.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy commons-io:commons-io:2.21.0=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy -io.dropwizard.metrics:metrics-core:3.2.5=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy +io.dropwizard.metrics:metrics-core:3.2.5=testRuntimeClasspathCopy +io.dropwizard.metrics:metrics-core:4.2.33=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,errorprone,testAnnotationProcessor io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,errorprone,testAnnotationProcessor io.netty:netty-buffer:4.2.12.Final=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy @@ -164,4 +165,4 @@ org.semver4j:semver4j:6.0.0=jarValidation,permitTestUnusedDeclared,testRuntimeCl org.slf4j:jcl-over-slf4j:2.0.17=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy org.slf4j:slf4j-api:2.0.17=compileClasspath,compileClasspathCopy,jarValidation,permitTestUnusedDeclared,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy org.xerial.snappy:snappy-java:1.1.10.8=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy -empty=apiHelper,apiHelperTest,compileOnlyHelperTest,missingdoclet,permitAggregatorUse,permitTestAggregatorUse,permitTestUsedUndeclared,permitUsedUndeclared +empty=apiHelper,apiHelperTest,apiHelperTestPrograms,compileOnlyHelperTest,compileOnlyHelperTestPrograms,missingdoclet,permitAggregatorUse,permitTestAggregatorUse,permitTestProgramsAggregatorUse,permitTestProgramsUnusedDeclared,permitTestProgramsUsedUndeclared,permitTestUsedUndeclared,permitUsedUndeclared,testProgramsAnnotationProcessor,testProgramsCompileClasspath,testProgramsRuntimeClasspath From 0afc2ee8cbf921877924bb305354127a4601fa21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 15:52:12 +0200 Subject: [PATCH 09/65] Fix changelog --- changelog/unreleased/15868-java-security-manager.yml | 6 ------ changelog/unreleased/SOLR-15868-java-agent.yml | 9 +++++++++ 2 files changed, 9 insertions(+), 6 deletions(-) delete mode 100644 changelog/unreleased/15868-java-security-manager.yml create mode 100644 changelog/unreleased/SOLR-15868-java-agent.yml diff --git a/changelog/unreleased/15868-java-security-manager.yml b/changelog/unreleased/15868-java-security-manager.yml deleted file mode 100644 index b7e82b16ecdb..000000000000 --- a/changelog/unreleased/15868-java-security-manager.yml +++ /dev/null @@ -1,6 +0,0 @@ -title: > - SOLR-17767: Introduce a Java agent (solr-agent-sm) that enforces file access, outbound network, - System.exit(), and process-spawn controls via ByteBuddy instrumentation. -type: added -authors: - - name: Jan Høydahl diff --git a/changelog/unreleased/SOLR-15868-java-agent.yml b/changelog/unreleased/SOLR-15868-java-agent.yml new file mode 100644 index 000000000000..a6f6a991c4b6 --- /dev/null +++ b/changelog/unreleased/SOLR-15868-java-agent.yml @@ -0,0 +1,9 @@ +title: > + Introduce a Java agent (solr-agent-sm) that enforces file access, outbound network, + System.exit(), and process-spawn controls via ByteBuddy instrumentation. +type: added +authors: + - name: Jan Høydahl +links: + - name: SOLR-15868 + url: https://issues.apache.org/jira/browse/SOLR-15868 From dcee23a8f83ba8bece06b2e184fe5989a3b2be58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 16:39:08 +0200 Subject: [PATCH 10/65] =?UTF-8?q?SOLR-17767:=20Address=20code=20review=20f?= =?UTF-8?q?indings=20=E2=80=94=20security=20correctness,=20chain-walk=20ex?= =?UTF-8?q?ec,=20codeBase=20matching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part A — stale class names: - dev-docs/security-agent.adoc: SolrSecurityPolicy → AgentPolicy, StackInspector → StackCallerClassChainExtractor - build.gradle comment: SolrSecurityPolicy, FileAccessInterceptor → AgentPolicy, FileInterceptor B1 — FileInterceptor "read" was a no-op; add explicit policy check for Files.read() path B2 — Remove tautological `args instanceof Object` guard (always true) B4 — SocketChannelInterceptor: implement codeBase matching via StackWalker instead of silently skipping codeBase-scoped entries; add isCallerFromCodeBase() helper + tests B5 — ProcessExecInterceptor: walk full chain (like SystemExitInterceptor) instead of checking only direct caller; add AgentPolicy.isChainThatCanExec() + tests B6 — Remove 0.0.0.0 from trustedHosts (unspecified bind address, not loopback) B7 — PermittedPath: use java.io.File.separator instead of hardcoded "/" and "\\" B8 — SolrAgentEntryPoint: .or(isAbstract()) → .and(not(isAbstract())) in file method matcher B9 — FileInterceptor: remove redundant second getCallerClass() call inside delete branch B10 — AgentPolicy.resetForTesting(): add comment explaining deliberate unsynchronized write B11 — Rename bootOtel → bootAgent in SolrAgentEntryPoint B12 — Fix typo: testApprovedCallerAnywherInChain → testApprovedCallerAnywhereInChain B13 — FileInterceptor: move resolveRealPath() Javadoc above the correct method; note why toRealPath() must not be called from the advice (circular interception) B14 — ViolationMetricsReporter.findMethod(): match by param type simple names, not just count B15 — SecurityViolationLogger: clarify stack-trace comment; deduplicate debug block --- NOTICE.txt | 3 + dev-docs/security-agent.adoc | 13 ++-- .../randomization/policies/solr-tests.policy | 4 -- solr/agent-sm/build.gradle | 6 +- .../solr/security/agent/AgentPolicy.java | 23 +++++++ .../solr/security/agent/FileInterceptor.java | 46 +++++++------ .../solr/security/agent/PermittedPath.java | 16 +++-- .../solr/security/agent/PolicyLoader.java | 4 +- .../agent/ProcessExecInterceptor.java | 27 +++++--- .../agent/SecurityViolationLogger.java | 13 ++-- .../agent/SocketChannelInterceptor.java | 67 ++++++++++++++++++- .../security/agent/SolrAgentEntryPoint.java | 6 +- .../agent/ViolationMetricsReporter.java | 29 ++++++-- .../agent/ProcessExecInterceptorTest.java | 41 ++++++++++++ .../agent/SocketChannelInterceptorTest.java | 28 +++++++- .../agent/SystemExitInterceptorTest.java | 2 +- solr/bin/solr.in.cmd | 2 - solr/bin/solr.in.sh | 2 - 18 files changed, 254 insertions(+), 78 deletions(-) diff --git a/NOTICE.txt b/NOTICE.txt index 579b6fffdd8e..0d3aaceed2bd 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -653,6 +653,9 @@ The S3 Output Stream is based on ASL 2.0 reference implementations found at: This files resides at: modules/s3-repository/src/java/org/apache/solr/s3/S3OutputStream.java +========================================================================= +== java agent notice == +========================================================================= This project contains source files in the "solr/agent-sm" module that were copied and adapted from the OpenSearch project (https://github.com/opensearch-project/OpenSearch), git tag "3.6.0", originally diff --git a/dev-docs/security-agent.adoc b/dev-docs/security-agent.adoc index c97840518183..b0981ef185f8 100644 --- a/dev-docs/security-agent.adoc +++ b/dev-docs/security-agent.adoc @@ -28,14 +28,14 @@ Key classes: * `SolrAgentEntryPoint` — `premain()` and `agentmain()` entry points; wires all ByteBuddy interceptors * `PolicyLoader` — parses `.policy` files and performs variable substitution -* `SolrSecurityPolicy` — immutable singleton holding the merged policy -* `StackInspector` — `StackWalker`-based call chain analysis (virtual-thread safe) +* `AgentPolicy` — immutable singleton holding the merged policy +* `StackCallerClassChainExtractor` — `StackWalker`-based call chain analysis (virtual-thread safe) * `SecurityViolationLogger` — emits structured SLF4J log entries for violations * `ViolationMetricsReporter` — `LongAdder` counters per violation type; deferred registration with `SolrMetricManager` == Adding an Approved Exit Caller -To allow a new class to call `System.exit()` or `Runtime.halt()`, add it to the approved callers list in `SolrSecurityPolicy`. +To allow a new class to call `System.exit()` or `Runtime.halt()`, add it to the approved callers list in `AgentPolicy`. The list is populated from the policy file using `java.lang.RuntimePermission "exitVM"` entries. Add an entry to `agent-security.policy`: @@ -46,18 +46,18 @@ grant codeBase "file:${solr.install.dir}/..." { }; ---- -Alternatively, add the class name to the default approved list in `SolrSecurityPolicy` if it is always a trusted Solr internal class. +Alternatively, add the class name to the default approved list in `AgentPolicy` if it is always a trusted Solr internal class. == Adding an Approved Exec Caller The default exec allow-list is empty — `ProcessBuilder.start()` and `Runtime.exec()` are blocked for all callers. -To permit a specific class, add a `java.lang.RuntimePermission "exec"` entry to the policy or the `SolrSecurityPolicy` defaults. +To permit a specific class, add a `java.lang.RuntimePermission "exec"` entry to the policy or the `AgentPolicy` defaults. Document the reason in a code comment. == Adding a Trusted Filesystem Scheme Paths on certain virtual filesystems (e.g., `jrt:/` for JDK resources) are excluded from file policy checks. -To add a trusted scheme, extend the `trustedFileSystems` set in `SolrSecurityPolicy`. +To add a trusted scheme, extend the `trustedFileSystems` set in `AgentPolicy`. Trusted schemes bypass all file access checks. == Deferred Metrics Registration @@ -97,7 +97,6 @@ Before flipping to enforce mode: 1. Run the full suite in warn mode and collect all `SECURITY VIOLATION` log entries. 2. Triage each violation — legitimate Solr operations need policy entries; plugin violations need plugin fixes or policy entries. 3. Update `agent-security.policy` or test-specific extra-policy files as needed. -4. File a JIRA (child of SOLR-17767) tracking this work before flipping. == No Compile-Time Dependency from solr:core on solr:agent-sm diff --git a/gradle/testing/randomization/policies/solr-tests.policy b/gradle/testing/randomization/policies/solr-tests.policy index d3784039f997..fe21f45a04c3 100644 --- a/gradle/testing/randomization/policies/solr-tests.policy +++ b/gradle/testing/randomization/policies/solr-tests.policy @@ -15,10 +15,6 @@ * limitations under the License. */ -// NOTE: The JSM policy sections in this file are no longer enforced by the JVM — the Java Security -// Manager API was removed in JDK 24. This file is retained for use by the randomized test -// framework infrastructure and will be reviewed for removal in a future release. - // Policy file for solr. Please keep minimal and avoid wildcards. // permissions needed for tests to pass, based on properties set by the build system diff --git a/solr/agent-sm/build.gradle b/solr/agent-sm/build.gradle index 57e96147de8f..6da7b5f4d7b9 100644 --- a/solr/agent-sm/build.gradle +++ b/solr/agent-sm/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'java-library' -description = 'Apache Solr Security Agent (Java Security Manager replacement)' +description = 'Apache Solr Security Agent' dependencies { // ByteBuddy references SpotBugs annotations in its class files; needed to suppress -Xlint:classfile @@ -58,10 +58,6 @@ jar { 'Agent-Class': 'org.apache.solr.security.agent.SolrAgentEntryPoint', 'Can-Redefine-Classes': 'true', 'Can-Retransform-Classes': 'true', - // Boot-Class-Path adds this JAR to the bootstrap classloader so that agent classes - // (SolrSecurityPolicy, FileAccessInterceptor, etc.) are resolvable when ByteBuddy - // inlines advice bytecode into bootstrap-loaded JDK classes like java.nio.file.Files. - // The bootstrap loader takes precedence, so there is only one copy of each class. 'Boot-Class-Path': archiveFileName ) } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java index dd2085647c2c..219cdf631c9e 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java @@ -144,6 +144,10 @@ public static boolean isInitialized() { * *

        This method is package-private and intended exclusively for unit tests in the {@code * org.apache.solr.security.agent} package. Production code must never call this method. + * + *

        The write is not synchronized: {@code instance} is {@code volatile}, so the assignment is + * immediately visible to all threads. Unlike {@link #initialize}, there is no invariant to + * protect here — tests call this only from a single thread during teardown. */ static void resetForTesting() { instance = null; @@ -255,4 +259,23 @@ public boolean isChainThatCanExit(Collection> chain) { } return false; } + + /** + * Returns {@code true} if any class in the call chain is approved to spawn child processes via + * {@code ProcessBuilder.start()} or {@code Runtime.exec()}. Any approved class anywhere in the + * chain grants permission, mirroring the exit-caller semantics in {@link + * #isChainThatCanExit(Collection)}. + * + * @param chain the full set of non-hidden classes in the call stack + */ + public boolean isChainThatCanExec(Collection> chain) { + for (Class cls : chain) { + for (ApprovedCallSite cs : approvedExecCallers) { + if (cs.operation() == ApprovedCallSite.Operation.EXEC && cs.matches(cls.getName())) { + return true; + } + } + } + return false; + } } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java index cfa2e82c9df3..5e43d1d66939 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java @@ -79,19 +79,10 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin boolean isUnixSocketCaller = false; if (isDelete == true) { final Collection> chain = walker.walk(StackCallerClassChainExtractor.INSTANCE); - - if (walker - .getCallerClass() - .getName() - .equalsIgnoreCase("sun.nio.ch.PipeImpl$Initializer$LoopbackConnector") - == true) { - isUnixSocketCaller = true; - } else { - for (final Class cls : chain) { - if (cls.getName().equalsIgnoreCase("sun.nio.ch.PipeImpl$Initializer$LoopbackConnector")) { - isUnixSocketCaller = true; - break; - } + for (final Class cls : chain) { + if (cls.getName().equalsIgnoreCase("sun.nio.ch.PipeImpl$Initializer$LoopbackConnector")) { + isUnixSocketCaller = true; + break; } } } @@ -103,7 +94,7 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin String targetFilePath = null; if (isMutating == false && isDelete == false) { if (name.equals("newByteChannel") == true || name.equals("open") == true) { - if (args.length > 1 && args instanceof Object) { + if (args.length > 1) { if (args instanceof OpenOption[] opts) { for (final OpenOption opt : opts) { if (opt != StandardOpenOption.READ) { @@ -189,6 +180,19 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin } } + // Plain read operations (e.g. Files.read(), Files.readAllBytes()) + if (name.equals("read") && !policy.isPathPermitted(filePath, "read")) { + ViolationMetricsReporter.incrementFile(); + SecurityViolationLogger.log( + SecurityViolationLogger.ViolationType.FILE_READ, + filePath, + caller, + policy.enforcementMode()); + if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException("Denied READ access to file: " + filePath); + } + } + // File mutating operations if (isMutating && !policy.isPathPermitted(filePath, "write")) { ViolationMetricsReporter.incrementFile(); @@ -221,11 +225,6 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin // Static helpers (used by advice and by tests) // --------------------------------------------------------------------------- - /** - * Resolves the real path of {@code path}, following symlinks. Falls back to {@code - * normalize().toAbsolutePath()} if the file does not exist or if a security manager blocks the - * call. - */ public static String topCallerClassName() { try { return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) @@ -236,6 +235,15 @@ public static String topCallerClassName() { } } + /** + * Resolves the real path of {@code path}, following symlinks. Falls back to {@code + * normalize().toAbsolutePath()} if the file does not exist or if an I/O error occurs. + * + *

        Note: This method must NOT be called from the ByteBuddy {@link #intercept} advice + * method — {@code toRealPath()} performs file-system I/O which would trigger re-entrant + * interception and cause infinite recursion. It is safe to use only from the test-side {@link + * #checkPath} helper where no live instrumentation is active. + */ public static String resolveRealPath(Path path) { try { return path.toRealPath(new LinkOption[0]).toString(); diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java index 6602be6f4f70..429d33a635a1 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java @@ -16,6 +16,7 @@ */ package org.apache.solr.security.agent; +import java.io.File; import java.util.Locale; /** @@ -61,12 +62,15 @@ public PolicyLoader.PolicySource source() { /** Returns {@code true} if this rule permits the given action on the given resolved path. */ public boolean permits(String resolvedPath, String action) { - boolean pathMatch = - recursive - ? resolvedPath.startsWith(path + "/") - || resolvedPath.startsWith(path + "\\") - || resolvedPath.equals(path) - : resolvedPath.equals(path); + boolean pathMatch; + if (recursive) { + // Use the platform separator so the check is correct on both POSIX and Windows. + // Strip any trailing separator from 'path' to avoid double-separator issues. + String base = path.endsWith(File.separator) ? path : path + File.separator; + pathMatch = resolvedPath.startsWith(base) || resolvedPath.equals(path); + } else { + pathMatch = resolvedPath.equals(path); + } return pathMatch && actions.contains(action.toLowerCase(Locale.ROOT)); } } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java index b458d9f36f2f..ac548349178c 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java @@ -229,8 +229,8 @@ private AgentPolicy buildPolicy(List grants) { ? AgentPolicy.EnforcementMode.ENFORCE : AgentPolicy.EnforcementMode.WARN; - Set trustedHosts = - Set.of("localhost", "127.0.0.1", "0:0:0:0:0:0:0:1", "::1", "0.0.0.0"); + // 0.0.0.0 is the unspecified bind address, not a loopback — intentionally excluded. + Set trustedHosts = Set.of("localhost", "127.0.0.1", "0:0:0:0:0:0:0:1", "::1"); return new AgentPolicy( paths, endpoints, exitCallers, execCallers, mode, Set.of(), trustedHosts); } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java index 548c41ee06a6..4c9e807a04bb 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java @@ -17,6 +17,7 @@ package org.apache.solr.security.agent; import java.lang.reflect.Method; +import java.util.Collection; import net.bytebuddy.asm.Advice; /** @@ -27,8 +28,10 @@ * policy (the list is empty), so all process-spawning attempts will be flagged unless an operator * explicitly adds an entry to {@code agent-security-extra.policy}. * - *

        This is a Solr-specific interceptor to cover the {@code ProcessBuilder} usage sites in Solr - * core (FR-007). + *

        Permission is granted if any class in the full call chain matches an approved entry — + * the same semantics as {@link SystemExitInterceptor}. This means that if an approved class (e.g. + * {@code SolrCLI}) calls a helper that then calls {@code ProcessBuilder.start()}, the helper call + * is still permitted. * *

        Known legitimate process-spawning call sites in Solr (kept out of the default production * policy because they use {@code ProcessHandle}, not {@code ProcessBuilder}): @@ -72,8 +75,10 @@ public static String deriveTarget(String methodName, Object[] args) { // --------------------------------------------------------------------------- /** - * Checks whether the current top caller is in the approved exec-caller list. Delegates to {@link - * SecurityViolationLogger} on violation. + * Checks whether any class in the current call chain is in the approved exec-caller list. Uses + * the full chain walk (same semantics as {@link SystemExitInterceptor}) so that an approved class + * anywhere in the stack grants permission. Delegates to {@link SecurityViolationLogger} on + * violation. * * @param target a human-readable description of the intercepted call for the violation log */ @@ -81,21 +86,21 @@ public static void checkExec(String target) { if (!AgentPolicy.isInitialized()) return; AgentPolicy policy = AgentPolicy.getInstance(); - String caller = - StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) - .getCallerClass() - .getName(); + final StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); + final Class caller = walker.getCallerClass(); + final Collection> chain = walker.walk(StackCallerClassChainExtractor.INSTANCE); - if (!policy.isExecApproved(caller)) { + if (!policy.isChainThatCanExec(chain)) { ViolationMetricsReporter.incrementExec(); SecurityViolationLogger.log( SecurityViolationLogger.ViolationType.PROCESS_EXEC, target, - caller, + caller.getName(), policy.enforcementMode()); if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { throw new SecurityException( - "Process spawning denied by Solr security agent — unapproved caller: " + caller); + "Process spawning denied by Solr security agent — unapproved caller: " + + caller.getName()); } } } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java index 3c3150910d43..d020bacf9e97 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java @@ -81,16 +81,13 @@ public static void log( if (mode == AgentPolicy.EnforcementMode.ENFORCE) { log.error(message); - if (log.isDebugEnabled()) { - log.debug( - "Stack trace for violation above:", new RuntimeException("violation stack trace")); - } } else { log.warn(message); - if (log.isDebugEnabled()) { - log.debug( - "Stack trace for violation above:", new RuntimeException("violation stack trace")); - } + } + // When DEBUG logging is enabled, emit the current call stack so the violation origin is visible + // in logs. The RuntimeException is never thrown — it is a carrier for the stack trace only. + if (log.isDebugEnabled()) { + log.debug("Call stack at point of violation:", new RuntimeException("call stack")); } } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java index b9f3b8c3bf52..8da12a5db01a 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java @@ -19,6 +19,8 @@ import java.lang.reflect.Method; import java.net.InetSocketAddress; import java.net.UnixDomainSocketAddress; +import java.security.CodeSource; +import java.util.Collection; import net.bytebuddy.asm.Advice; import net.bytebuddy.asm.Advice.Origin; @@ -132,11 +134,72 @@ public static String topCallerClassName() { *

      • Entry {@code host:low-high} — matches the host with a port in the inclusive range *
      • Entry {@code *} (no colon) — matches everything (broad wildcard) *
      + * + *

      Entries with a {@code codeBase} restriction are evaluated against the current call chain via + * {@link StackWalker}: the entry permits the connection only if at least one class in the chain + * was loaded from a code source under that codeBase path. The stack walk is performed lazily — + * only when a codeBase-restricted entry whose endpoint pattern matches is encountered. */ public static boolean isEndpointPermitted(AgentPolicy policy, String host, int port) { + Collection> chain = null; // lazily populated + for (PermittedEndpoint entry : policy.permittedEndpoints()) { - if (entry.codeBase() != null) continue; - if (matchesEndpoint(entry.hostPort(), host, port)) return true; + if (!matchesEndpoint(entry.hostPort(), host, port)) continue; + + if (entry.codeBase() == null) { + // Global grant — no code-source restriction + return true; + } + + // codeBase-scoped grant: check if any class in the call chain was loaded from that codeBase + if (chain == null) { + chain = + StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(StackCallerClassChainExtractor.INSTANCE); + } + if (isCallerFromCodeBase(chain, entry.codeBase())) return true; + } + return false; + } + + /** + * Returns {@code true} if any class in {@code chain} has a code source whose location is under + * the given {@code codeBase} path. Supports JDK policy file {@code codeBase} syntax: + * + *

        + *
      • {@code file:/path/to/dir/-} — recursive: matches any JAR or class in that directory tree + *
      • {@code file:/path/to/dir/} or {@code file:/path/to/dir} — exact directory + *
      • {@code file:/path/to/specific.jar} — exact JAR file + *
      + */ + static boolean isCallerFromCodeBase(Collection> chain, String codeBase) { + // Strip "file:" scheme prefix if present + String base = codeBase.startsWith("file:") ? codeBase.substring(5) : codeBase; + boolean recursive = base.endsWith("/-"); + if (recursive) base = base.substring(0, base.length() - 2); + // Normalise: strip trailing "/" so startsWith checks are consistent + while (base.endsWith("/") || base.endsWith("\\")) base = base.substring(0, base.length() - 1); + + for (Class cls : chain) { + try { + CodeSource cs = cls.getProtectionDomain().getCodeSource(); + if (cs == null) continue; + java.net.URL loc = cs.getLocation(); + if (loc == null) continue; + String locPath = loc.getPath(); + if (locPath == null) continue; + // Normalise: strip trailing separators + while (locPath.endsWith("/") || locPath.endsWith("\\")) + locPath = locPath.substring(0, locPath.length() - 1); + + if (recursive) { + if (locPath.equals(base) || locPath.startsWith(base + "/")) return true; + } else { + if (locPath.equals(base)) return true; + } + } catch (Exception ignored) { + // SecurityException or other runtime exception — skip this frame + } } return false; } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java index 89f506d57cc4..294aa849bcad 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java @@ -64,7 +64,7 @@ private SolrAgentEntryPoint() {} * @param inst the {@link Instrumentation} instance provided by the JVM */ public static void premain(String agentArgs, Instrumentation inst) { - bootOtel(inst); + bootAgent(inst); } /** @@ -87,7 +87,7 @@ public static void agentmain(String agentArgs, Instrumentation inst) { "System.err is the only output available during premain (before logging is initialized). " + "System.exit(1) is required to halt the JVM on a fatal policy-load failure in " + "enforce mode — this is intentional agent behavior, not application code.") - private static void bootOtel(Instrumentation inst) { + private static void bootAgent(Instrumentation inst) { // Locate the default policy file next to the agent JAR. Path defaultPolicyPath = resolveDefaultPolicyPath(); @@ -182,7 +182,7 @@ private static void installInterceptors(Instrumentation inst) { Advice.to(FileInterceptor.class) .on( ElementMatchers.namedOneOf(FILE_INTERCEPTED_METHODS) - .or(ElementMatchers.isAbstract())))) + .and(ElementMatchers.not(ElementMatchers.isAbstract()))))) // Intercept SocketChannel / Socket outbound connections → SocketChannelInterceptor .type( ElementMatchers.isSubTypeOf(java.nio.channels.SocketChannel.class) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java index 6e518128b7f0..f057aa457ace 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java @@ -144,7 +144,16 @@ public static void registerWithSolrMetrics(Object metricManager, String registry Class mmClass = metricManager.getClass(); // SolrMetricManager.observableLongCounter(String registry, String name, String description, // Consumer callback, OtelUnit unit) - Method counterMethod = findMethod(mmClass, "observableLongCounter", 5); + // SolrMetricManager.observableLongCounter(String, String, String, Consumer, OtelUnit) + Method counterMethod = + findMethod( + mmClass, + "observableLongCounter", + "String", + "String", + "String", + "Consumer", + "OtelUnit"); if (counterMethod == null) { agentErr( "[Solr SecurityAgent] SolrMetricManager.observableLongCounter not found" @@ -185,11 +194,23 @@ public static void registerWithSolrMetrics(Object metricManager, String registry } } - private static Method findMethod(Class cls, String name, int paramCount) { + /** + * Finds a public method on {@code cls} by name and parameter type simple-names. Using simple + * names avoids a compile-time dependency on {@code solr:core} types (e.g. {@code OtelUnit}). + */ + private static Method findMethod(Class cls, String name, String... paramTypeSimpleNames) { for (Method m : cls.getMethods()) { - if (name.equals(m.getName()) && m.getParameterCount() == paramCount) { - return m; + if (!name.equals(m.getName())) continue; + Class[] params = m.getParameterTypes(); + if (params.length != paramTypeSimpleNames.length) continue; + boolean match = true; + for (int i = 0; i < params.length; i++) { + if (!params[i].getSimpleName().equals(paramTypeSimpleNames[i])) { + match = false; + break; + } } + if (match) return m; } return null; } diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java index 5136aadf1f3e..4c18c5d57196 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java @@ -16,8 +16,11 @@ */ package org.apache.solr.security.agent; +import java.util.Collection; import java.util.List; +import java.util.Set; import org.apache.solr.SolrTestCase; +import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -31,6 +34,11 @@ public void snapshotCounters() { execCountBefore = ViolationMetricsReporter.execCount(); } + @After + public void resetSingleton() { + AgentPolicy.resetForTesting(); + } + private void initPolicy(boolean approved, String callerClass, AgentPolicy.EnforcementMode mode) { resetPolicySingleton(); List execCallers = @@ -93,4 +101,37 @@ public void testWildcardApprovalMatchesAny() { assertEquals(execCountBefore, ViolationMetricsReporter.execCount()); resetPolicySingleton(); } + + @Test + public void testApprovedCallerAnywhereInChainPermits() { + // isChainThatCanExec mirrors isChainThatCanExit: any approved class anywhere grants permission. + AgentPolicy.resetForTesting(); + List execCallers = + List.of( + new ApprovedCallSite( + ProcessExecInterceptorTest.class.getName(), + ApprovedCallSite.Operation.EXEC, + PolicyLoader.PolicySource.DEFAULT)); + AgentPolicy policy = + new AgentPolicy( + List.of(), List.of(), List.of(), execCallers, AgentPolicy.EnforcementMode.ENFORCE); + AgentPolicy.initialize(policy); + + // Approved class is deep in the chain — should still grant permission + Collection> chain = + Set.of(String.class, Integer.class, ProcessExecInterceptorTest.class); + assertTrue(policy.isChainThatCanExec(chain)); + } + + @Test + public void testUnapprovedChainDenies() { + AgentPolicy.resetForTesting(); + AgentPolicy policy = + new AgentPolicy( + List.of(), List.of(), List.of(), List.of(), AgentPolicy.EnforcementMode.ENFORCE); + AgentPolicy.initialize(policy); + + Collection> chain = Set.of(String.class, Integer.class); + assertFalse(policy.isChainThatCanExec(chain)); + } } diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java index 0948f433d8ba..d31a9417bbb5 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java @@ -18,6 +18,7 @@ import java.net.InetAddress; import java.net.InetSocketAddress; +import java.util.Collection; import java.util.List; import java.util.Set; import org.apache.solr.SolrTestCase; @@ -85,12 +86,14 @@ public void testPortRangePermitted() { } @Test - public void testCodebaseScopedEntrySkipped() { + public void testCodebaseScopedEntryDeniedWhenCallerNotFromThatCodeBase() { + // A codeBase grant for a path where no class in the current test stack is loaded from. + // The call chain contains test-framework classes, not classes from /nonexistent/path. PermittedEndpoint codeBasedEp = new PermittedEndpoint( "*", "connect,resolve", - "file:/opt/solr/modules/jwt-auth/-", + "file:/nonexistent/solr/modules/jwt-auth/-", PolicyLoader.PolicySource.DEFAULT); AgentPolicy policy = new AgentPolicy( @@ -102,6 +105,27 @@ public void testCodebaseScopedEntrySkipped() { assertFalse(SocketChannelInterceptor.isEndpointPermitted(policy, "external.host", 443)); } + @Test + public void testIsCallerFromCodeBaseMatchesTestClassCodeSource() { + // Get the actual code source URL of this test class and verify isCallerFromCodeBase accepts it. + java.net.URL loc = + SocketChannelInterceptorTest.class.getProtectionDomain().getCodeSource().getLocation(); + String codeBase = "file:" + loc.getPath(); + // Strip any trailing "/" so it acts as an exact-directory match + if (codeBase.endsWith("/")) codeBase = codeBase.substring(0, codeBase.length() - 1); + // Append "/-" to make it a recursive match (any class under the test build dir) + codeBase = codeBase + "/-"; + + Collection> chain = Set.of(SocketChannelInterceptorTest.class); + assertTrue(SocketChannelInterceptor.isCallerFromCodeBase(chain, codeBase)); + } + + @Test + public void testIsCallerFromCodeBaseNoMatchForUnrelatedPath() { + Collection> chain = Set.of(SocketChannelInterceptorTest.class); + assertFalse(SocketChannelInterceptor.isCallerFromCodeBase(chain, "file:/nonexistent/path/-")); + } + @Test public void testUnlistedHostPortBlocked() { AgentPolicy policy = policyWithEndpoint("localhost:8983"); diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SystemExitInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SystemExitInterceptorTest.java index 1b86208e0095..7e130ba3e442 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SystemExitInterceptorTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SystemExitInterceptorTest.java @@ -68,7 +68,7 @@ public void testUnapprovedCallerNotInChainDenies() { } @Test - public void testApprovedCallerAnywherInChainPermits() { + public void testApprovedCallerAnywhereInChainPermits() { AgentPolicy policy = initPolicy( true, SystemExitInterceptorTest.class.getName(), AgentPolicy.EnforcementMode.ENFORCE); diff --git a/solr/bin/solr.in.cmd b/solr/bin/solr.in.cmd index 4a4fbd36643a..0ad8cc5ec27c 100755 --- a/solr/bin/solr.in.cmd +++ b/solr/bin/solr.in.cmd @@ -228,14 +228,12 @@ REM automatically. See the Solr Reference Guide for full documentation. REM Enforcement mode: "warn" (log violations, allow operation — default) or REM "enforce" (log violations, block operation with SecurityException). -REM The startup script converts this env var to -Dsolr.security.agent.mode for the agent. REM set SOLR_SECURITY_AGENT_MODE=warn REM Set to "true" to completely disable the security agent (for troubleshooting only). REM set SOLR_SECURITY_AGENT_SKIP=false REM Path to the operator extension policy file (default: server\etc\agent-security-extra.policy). -REM The startup script converts this env var to -Dsolr.security.agent.extra.policy for the agent. REM set SOLR_SECURITY_AGENT_EXTRA_POLICY=C:\path\to\custom-agent-security-extra.policy REM This variable provides you with the option to disable the Admin UI. if you uncomment the variable below and diff --git a/solr/bin/solr.in.sh b/solr/bin/solr.in.sh index f78dc61101a4..ea7d442c7f6e 100644 --- a/solr/bin/solr.in.sh +++ b/solr/bin/solr.in.sh @@ -254,7 +254,6 @@ # Enforcement mode: "warn" (log violations, allow operation — default) or # "enforce" (log violations, block operation with SecurityException). -# The startup script converts this env var to -Dsolr.security.agent.mode for the agent. #SOLR_SECURITY_AGENT_MODE=warn # Set to "true" to completely disable the security agent (for troubleshooting only). @@ -264,7 +263,6 @@ # Path to the operator extension policy file. # Defaults to ${server.dir}/etc/agent-security-extra.policy when unset. # Override to support read-only install trees, container images, or config-management tooling. -# The startup script converts this env var to -Dsolr.security.agent.extra.policy for the agent. #SOLR_SECURITY_AGENT_EXTRA_POLICY=/path/to/custom-agent-security-extra.policy # This variable provides you with the option to disable the Admin UI. If you uncomment the variable below and From 2fbd5856a242ffdbccf388d17bbf00eb6260fe5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 16:55:10 +0200 Subject: [PATCH 11/65] SOLR-17767: Prefix security agent metric names with solr.* per Solr conventions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All Solr metrics should start with "solr.*". Rename: security.agent.violations.{file,network,exit,exec} → solr.security.agent.violations.{file,network,exit,exec} In Prometheus format: solr_security_agent_violations_*_total. Update BATS test assertion, ref-guide, and Javadoc accordingly. --- .../agent/ViolationMetricsReporter.java | 20 +++++++++---------- solr/packaging/test/test_security_agent.bats | 8 ++++---- .../pages/security-agent.adoc | 14 ++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java index f057aa457ace..a36c9806f82b 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java @@ -37,13 +37,13 @@ *

      Metric names

      * *
        - *
      • {@code security.agent.violations.file} - *
      • {@code security.agent.violations.network} - *
      • {@code security.agent.violations.exit} - *
      • {@code security.agent.violations.exec} + *
      • {@code solr.security.agent.violations.file} + *
      • {@code solr.security.agent.violations.network} + *
      • {@code solr.security.agent.violations.exit} + *
      • {@code solr.security.agent.violations.exec} *
      * - *

      In Prometheus format these appear as {@code security_agent_violations_file_total}, etc. + *

      In Prometheus format these appear as {@code solr_security_agent_violations_file_total}, etc. */ public final class ViolationMetricsReporter { @@ -54,10 +54,10 @@ public final class ViolationMetricsReporter { private static final LongAdder EXEC_COUNTER = new LongAdder(); // Metric names exposed in the Solr metrics registry. - public static final String METRIC_FILE = "security.agent.violations.file"; - public static final String METRIC_NETWORK = "security.agent.violations.network"; - public static final String METRIC_EXIT = "security.agent.violations.exit"; - public static final String METRIC_EXEC = "security.agent.violations.exec"; + public static final String METRIC_FILE = "solr.security.agent.violations.file"; + public static final String METRIC_NETWORK = "solr.security.agent.violations.network"; + public static final String METRIC_EXIT = "solr.security.agent.violations.exit"; + public static final String METRIC_EXEC = "solr.security.agent.violations.exec"; private ViolationMetricsReporter() {} @@ -133,7 +133,7 @@ public static long execCount() { * *

      Metrics are registered as OTel observable counters via {@code * SolrMetricManager.observableLongCounter()}. In Prometheus format they appear as {@code - * security_agent_violations_file_total} etc. + * solr_security_agent_violations_file_total} etc. * * @param metricManager the {@code SolrMetricManager} instance (type-erased to {@link Object} to * avoid a compile-time dependency on solr:core) diff --git a/solr/packaging/test/test_security_agent.bats b/solr/packaging/test/test_security_agent.bats index 65062b58ae7e..03bbf1d11a51 100644 --- a/solr/packaging/test/test_security_agent.bats +++ b/solr/packaging/test/test_security_agent.bats @@ -59,12 +59,12 @@ console_log() { assert_output --partial "mode=WARN" # Confirms: CoreContainer reflective call to ViolationMetricsReporter registered counters. - # Metrics appear in Prometheus format (security.agent.violations.file → - # security_agent_violations_file_total) since SolrMetricManager is OTel-based. + # Metrics appear in Prometheus format (solr.security.agent.violations.file → + # solr_security_agent_violations_file_total) since SolrMetricManager is OTel-based. run curl -sf "http://localhost:${SOLR_PORT}/solr/admin/metrics" assert_success - assert_output --partial "security_agent_violations_file" - assert_output --partial "security_agent_violations_network" + assert_output --partial "solr_security_agent_violations_file" + assert_output --partial "solr_security_agent_violations_network" } @test "SOLR_SECURITY_AGENT_SKIP=true disables the agent" { diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc index ff9f3e0c2830..8ce6a26beab2 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc @@ -22,7 +22,7 @@ The agent intercepts file access, network connections, `System.exit()`, and proc == Protections Active by Default When the agent JAR is present, the startup scripts automatically activate it. -The following protections apply to all code running inside the Solr JVM, including plugins: +The following protections apply to all code running inside the Solr JVM: * **File access** — reads and writes are restricted to permitted directories (Solr home, data dir, log dir, temp dir). * **Outbound network** — connections are restricted to loopback addresses and intra-cluster ports. @@ -66,7 +66,7 @@ Its location can be overridden via `SOLR_SECURITY_AGENT_EXTRA_POLICY`. === Variable Substitution -The following variables are expanded in policy entries: +Any java property will be expanded in policy entries, here are the most common. Environment variables are expanded if they adhere to the mapping convention `SOLR_FOO --> solr.foo`: [cols="1,2",options="header"] |=== @@ -129,10 +129,10 @@ Fields: Violation counts are also available in `/admin/metrics` under: -* `security.agent.violations.file` -* `security.agent.violations.network` -* `security.agent.violations.exit` -* `security.agent.violations.exec` +* `solr.security.agent.violations.file` +* `solr.security.agent.violations.network` +* `solr.security.agent.violations.exit` +* `solr.security.agent.violations.exec` == Disabling the Agent @@ -146,7 +146,7 @@ SOLR_SECURITY_AGENT_SKIP=true [WARNING] ==== Disabling the agent removes all runtime security controls. -Use this only for temporary troubleshooting and re-enable it as soon as possible. +Use this only for temporary troubleshooting of if you intend to provide similar protection on the operating system level. ==== == Modules with Pre-Permitted Network Access From 8a906ba16093d54703884eafb8ec07c2c2809a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 16:56:07 +0200 Subject: [PATCH 12/65] Other fixes --- solr/server/build.gradle | 2 +- solr/server/etc/agent-security.policy | 2 +- solr/server/etc/security.policy | 5 ----- .../modules/configuration-guide/pages/solr-properties.adoc | 6 +++--- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/solr/server/build.gradle b/solr/server/build.gradle index 8bb03ff15f58..771939f8b1ec 100644 --- a/solr/server/build.gradle +++ b/solr/server/build.gradle @@ -62,7 +62,7 @@ dependencies { serverLib libs.eclipse.jetty.http2.hpack serverLib libs.jakarta.servlet.api - // Security agent fat JAR — auto-detected by bin/solr and loaded via -javaagent:. + // Security agent JAR — auto-detected by bin/solr and loaded via -javaagent:. // transitive=false because ByteBuddy and SLF4J are already bundled inside the fat JAR. libExt(project(path: ':solr:agent-sm'), { transitive false diff --git a/solr/server/etc/agent-security.policy b/solr/server/etc/agent-security.policy index deba7a11033c..3c1aae44339b 100644 --- a/solr/server/etc/agent-security.policy +++ b/solr/server/etc/agent-security.policy @@ -60,7 +60,7 @@ grant { permission java.net.SocketPermission "127.0.0.1:1-65535", "connect,resolve"; permission java.net.SocketPermission "[::1]:1-65535", "connect,resolve"; - // Intra-cluster connectivity — Decision 9 (port-wildcard approach): + // Intra-cluster connectivity // Any host on the Solr HTTP port is permitted, covering all current and future cluster nodes // without requiring a static node list at startup. Similarly for the ZooKeeper port. permission java.net.SocketPermission "*:${solr.port.listen}", "connect,resolve"; diff --git a/solr/server/etc/security.policy b/solr/server/etc/security.policy index 14f63e76667d..f932cc0b461c 100644 --- a/solr/server/etc/security.policy +++ b/solr/server/etc/security.policy @@ -15,11 +15,6 @@ * limitations under the License. */ -// DEPRECATED: This file is no longer enforced by the JVM. The Java Security Manager (JSM) API -// was removed in JDK 24. This file is retained as a migration reference only and will be removed -// in a future Solr release. The replacement is agent-security.policy, loaded by the Solr security -// agent (solr-agent-sm-*.jar). See the Solr Reference Guide: Security Agent for details. - // Policy file for solr. Please keep minimal and avoid wildcards. // permissions needed for tests to pass, based on properties set by the build system diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/solr-properties.adoc b/solr/solr-ref-guide/modules/configuration-guide/pages/solr-properties.adoc index e473b5d0922e..5609226f591b 100644 --- a/solr/solr-ref-guide/modules/configuration-guide/pages/solr-properties.adoc +++ b/solr/solr-ref-guide/modules/configuration-guide/pages/solr-properties.adoc @@ -92,11 +92,11 @@ NOTE: Properties marked with "!" indicate inverted meaning between pre Solr 10 a |solr.responses.stacktrace.enabled|!solr.hideStackTrace|false|Controls whether stack traces are included in responses. When set to `true`, stack traces are included in responses. -|solr.security.agent.extra.policy|SOLR_SECURITY_AGENT_EXTRA_POLICY|`${server.dir}/etc/agent-security-extra.policy`|Path to the operator extension policy file for the Solr security agent. Overrides the default location. An absent file is silently skipped. Set env var `SOLR_SECURITY_AGENT_EXTRA_POLICY` to have the startup script pass this as a `-D` system property. +|solr.security.agent.extra.policy||`${server.dir}/etc/agent-security-extra.policy`|Path to the operator extension policy file for the Solr security agent. Overrides the default location. An absent file is silently skipped. Set env var `SOLR_SECURITY_AGENT_EXTRA_POLICY` to have the startup script pass this as a `-D` system property. -|solr.security.agent.mode|SOLR_SECURITY_AGENT_MODE|`warn`|Enforcement mode for the Solr security agent: `warn` (log violations, continue) or `enforce` (log violations, block the operation with `SecurityException`). Set env var `SOLR_SECURITY_AGENT_MODE` to have the startup script pass this as a `-D` system property. +|solr.security.agent.mode||`warn`|Enforcement mode for the Solr security agent: `warn` (log violations, continue) or `enforce` (log violations, block the operation with `SecurityException`). Set env var `SOLR_SECURITY_AGENT_MODE` to have the startup script pass this as a `-D` system property. -|SOLR_SECURITY_AGENT_SKIP|_(startup-script only, no system property)_|`false`|If set to `true`, omits the `-javaagent:` flag from the JVM command line, disabling all Solr security agent controls. Intended for temporary troubleshooting only. +|solr.security.agent.skip||`false`|If set to `true`, omits the `-javaagent:` flag from the JVM command line, disabling all Solr security agent controls. Intended for temporary troubleshooting only. |solr.security.auth.basicauth.credentials|basicauth||Defines basic authentication credentials. From 067ca3b55413c0085bcf9ff3e8028e0959cdfd0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 17:03:55 +0200 Subject: [PATCH 13/65] Remove untrue statement in solr-properties.adoc --- .../modules/configuration-guide/pages/solr-properties.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/solr-properties.adoc b/solr/solr-ref-guide/modules/configuration-guide/pages/solr-properties.adoc index 5609226f591b..0dd02cea779f 100644 --- a/solr/solr-ref-guide/modules/configuration-guide/pages/solr-properties.adoc +++ b/solr/solr-ref-guide/modules/configuration-guide/pages/solr-properties.adoc @@ -92,9 +92,9 @@ NOTE: Properties marked with "!" indicate inverted meaning between pre Solr 10 a |solr.responses.stacktrace.enabled|!solr.hideStackTrace|false|Controls whether stack traces are included in responses. When set to `true`, stack traces are included in responses. -|solr.security.agent.extra.policy||`${server.dir}/etc/agent-security-extra.policy`|Path to the operator extension policy file for the Solr security agent. Overrides the default location. An absent file is silently skipped. Set env var `SOLR_SECURITY_AGENT_EXTRA_POLICY` to have the startup script pass this as a `-D` system property. +|solr.security.agent.extra.policy||`${server.dir}/etc/agent-security-extra.policy`|Path to the operator extension policy file for the Solr security agent. Overrides the default location. An absent file is silently skipped. -|solr.security.agent.mode||`warn`|Enforcement mode for the Solr security agent: `warn` (log violations, continue) or `enforce` (log violations, block the operation with `SecurityException`). Set env var `SOLR_SECURITY_AGENT_MODE` to have the startup script pass this as a `-D` system property. +|solr.security.agent.mode||`warn`|Enforcement mode for the Solr security agent: `warn` (log violations, continue) or `enforce` (log violations, block the operation with `SecurityException`). |solr.security.agent.skip||`false`|If set to `true`, omits the `-javaagent:` flag from the JVM command line, disabling all Solr security agent controls. Intended for temporary troubleshooting only. From 04624e5e4704f36475885dd32a2682e3a91c4799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 17:08:34 +0200 Subject: [PATCH 14/65] =?UTF-8?q?SOLR-17767:=20Fix=20build=20issues=20?= =?UTF-8?q?=E2=80=94=20disable=20annotation=20processing=20for=20testProgr?= =?UTF-8?q?ams;=20use=20NIO=20FileSystems.getSeparator()=20in=20PermittedP?= =?UTF-8?q?ath?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - testProgramsAnnotationProcessor config was pulling in error-prone deps not in the dependency lock file; disable annotation processing for trivial test programs. - PermittedPath used java.io.File (forbidden API); switch to FileSystems.getDefault().getSeparator() per Solr's NIO-only policy. --- solr/agent-sm/build.gradle | 7 +++++++ .../java/org/apache/solr/security/agent/PermittedPath.java | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/solr/agent-sm/build.gradle b/solr/agent-sm/build.gradle index 6da7b5f4d7b9..9c43967d2676 100644 --- a/solr/agent-sm/build.gradle +++ b/solr/agent-sm/build.gradle @@ -102,6 +102,13 @@ sourceSets { } } +// These are tiny standalone programs with no annotation-processor needs. +// Disabling annotation processing prevents the auto-created testProgramsAnnotationProcessor +// configuration from pulling in error-prone and friends, which are not in the lock file. +tasks.named('compileTestProgramsJava') { + options.annotationProcessorPath = files() +} + task testProgramsJar(type: Jar) { description = 'JAR of standalone violation programs used by BATS integration tests' group = 'build' diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java index 429d33a635a1..e03fcb2303fb 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java @@ -16,7 +16,7 @@ */ package org.apache.solr.security.agent; -import java.io.File; +import java.nio.file.FileSystems; import java.util.Locale; /** @@ -66,7 +66,8 @@ public boolean permits(String resolvedPath, String action) { if (recursive) { // Use the platform separator so the check is correct on both POSIX and Windows. // Strip any trailing separator from 'path' to avoid double-separator issues. - String base = path.endsWith(File.separator) ? path : path + File.separator; + String sep = FileSystems.getDefault().getSeparator(); + String base = path.endsWith(sep) ? path : path + sep; pathMatch = resolvedPath.startsWith(base) || resolvedPath.equals(path); } else { pathMatch = resolvedPath.equals(path); From 6e811139d246a5c7aa75eca3c27aa9953ff819a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 17:12:12 +0200 Subject: [PATCH 15/65] Fix error-prone UnnecessarilyFullyQualified warnings in SocketChannelInterceptor Add `import java.net.URL;`, replace `java.net.URL loc` with `URL loc`, and replace `java.net.InetSocketAddress` parameter type with `InetSocketAddress` since the type is already imported. Fixes CI error-prone -Werror failures. --- .../apache/solr/security/agent/SocketChannelInterceptor.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java index 8da12a5db01a..3ed0808e51f0 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java @@ -18,6 +18,7 @@ import java.lang.reflect.Method; import java.net.InetSocketAddress; +import java.net.URL; import java.net.UnixDomainSocketAddress; import java.security.CodeSource; import java.util.Collection; @@ -87,7 +88,7 @@ public static void intercept(@Advice.AllArguments Object[] args, @Origin Method * *

      Used by tests to exercise the network check without ByteBuddy instrumentation. */ - public static void checkConnect(java.net.InetSocketAddress address) { + public static void checkConnect(InetSocketAddress address) { if (!AgentPolicy.isInitialized()) return; if (address.isUnresolved()) return; AgentPolicy policy = AgentPolicy.getInstance(); @@ -184,7 +185,7 @@ static boolean isCallerFromCodeBase(Collection> chain, String codeBase) try { CodeSource cs = cls.getProtectionDomain().getCodeSource(); if (cs == null) continue; - java.net.URL loc = cs.getLocation(); + URL loc = cs.getLocation(); if (loc == null) continue; String locPath = loc.getPath(); if (locPath == null) continue; From f63ab08d83bc739f4ffa9c9268897906c31d430d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 18:39:27 +0200 Subject: [PATCH 16/65] Fix error-prone UnnecessarilyFullyQualified warnings in SolrAgentEntryPoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add imports for SocketChannel, Socket and use simple names for System, Runtime, ProcessBuilder — all were already in java.lang or now imported. Fixes CI error-prone -Werror failures. --- .../solr/security/agent/SolrAgentEntryPoint.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java index 294aa849bcad..788a656eb033 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java @@ -17,7 +17,9 @@ package org.apache.solr.security.agent; import java.lang.instrument.Instrumentation; +import java.net.Socket; import java.nio.channels.FileChannel; +import java.nio.channels.SocketChannel; import java.nio.file.Path; import java.nio.file.spi.FileSystemProvider; import net.bytebuddy.ByteBuddy; @@ -185,8 +187,8 @@ private static void installInterceptors(Instrumentation inst) { .and(ElementMatchers.not(ElementMatchers.isAbstract()))))) // Intercept SocketChannel / Socket outbound connections → SocketChannelInterceptor .type( - ElementMatchers.isSubTypeOf(java.nio.channels.SocketChannel.class) - .or(ElementMatchers.isSubTypeOf(java.net.Socket.class))) + ElementMatchers.isSubTypeOf(SocketChannel.class) + .or(ElementMatchers.isSubTypeOf(Socket.class))) .transform( (builder, type, classLoader, module, domain) -> builder.visit( @@ -195,25 +197,25 @@ private static void installInterceptors(Instrumentation inst) { ElementMatchers.named("connect") .and(ElementMatchers.not(ElementMatchers.isAbstract()))))) // Intercept System.exit(int) → SystemExitInterceptor - .type(ElementMatchers.is(java.lang.System.class)) + .type(ElementMatchers.is(System.class)) .transform( (builder, type, classLoader, module, domain) -> builder.visit( Advice.to(SystemExitInterceptor.class).on(ElementMatchers.named("exit")))) // Intercept Runtime.halt(int) → RuntimeHaltInterceptor - .type(ElementMatchers.is(java.lang.Runtime.class)) + .type(ElementMatchers.is(Runtime.class)) .transform( (builder, type, classLoader, module, domain) -> builder.visit( Advice.to(RuntimeHaltInterceptor.class).on(ElementMatchers.named("halt")))) // Intercept ProcessBuilder.start() → ProcessExecInterceptor - .type(ElementMatchers.is(java.lang.ProcessBuilder.class)) + .type(ElementMatchers.is(ProcessBuilder.class)) .transform( (builder, type, classLoader, module, domain) -> builder.visit( Advice.to(ProcessExecInterceptor.class).on(ElementMatchers.named("start")))) // Intercept Runtime.exec(String[]) → ProcessExecInterceptor - .type(ElementMatchers.is(java.lang.Runtime.class)) + .type(ElementMatchers.is(Runtime.class)) .transform( (builder, type, classLoader, module, domain) -> builder.visit( From 8fa12769e78fb09ac53ce2d1bd1dc23773eb88e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 18:39:30 +0200 Subject: [PATCH 17/65] Fix precommit and wrong docs --- .../modules/deployment-guide/pages/security-agent.adoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc index 8ce6a26beab2..62757fecd073 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc @@ -48,8 +48,6 @@ Set the mode in `solr.in.sh` or `solr.in.cmd`: SOLR_SECURITY_AGENT_MODE=enforce ---- -The startup script converts this to the JVM system property `-Dsolr.security.agent.mode=enforce`. - == Policy File Format Solr uses JDK-style `.policy` files with Solr-specific variable substitution. From cfe0060c4d9f5c2be384416e91cf584a3848e6b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 18:42:21 +0200 Subject: [PATCH 18/65] Fix error-prone UnnecessarilyFullyQualified warning in ProcessExecInterceptor --- .../org/apache/solr/security/agent/ProcessExecInterceptor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java index 4c9e807a04bb..a7443ec0f78e 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java @@ -16,6 +16,7 @@ */ package org.apache.solr.security.agent; +import java.io.IOException; import java.lang.reflect.Method; import java.util.Collection; import net.bytebuddy.asm.Advice; @@ -54,7 +55,7 @@ private ProcessExecInterceptor() {} * @param args all arguments of the intercepted method * @param method the intercepted method (used to identify the call site in the violation log) */ - @Advice.OnMethodEnter(suppress = java.io.IOException.class) + @Advice.OnMethodEnter(suppress = IOException.class) public static void onExec(@Advice.AllArguments Object[] args, @Advice.Origin Method method) { checkExec(deriveTarget(method.getName(), args)); } From 761e9511c4dc53bbc538a89e4a7e9ba51ffabf7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 27 May 2026 18:50:55 +0200 Subject: [PATCH 19/65] Fix all remaining error-prone UnnecessarilyFullyQualified warnings in agent-sm - build.gradle: disable error-prone for compileTestProgramsJava (no errorprone JAR on testProgramsAnnotationProcessor config) - SymlinkEscapeTest, SocketChannelInterceptorTest, SolrAgentIntegrationTest: add missing imports and remove FQ type references All 59 tests pass with -Pvalidation.errorprone=true. --- solr/agent-sm/build.gradle | 9 +++++++++ .../security/agent/SocketChannelInterceptorTest.java | 3 ++- .../solr/security/agent/SolrAgentIntegrationTest.java | 8 ++++---- .../apache/solr/security/agent/SymlinkEscapeTest.java | 3 ++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/solr/agent-sm/build.gradle b/solr/agent-sm/build.gradle index 9c43967d2676..7461cb7634b0 100644 --- a/solr/agent-sm/build.gradle +++ b/solr/agent-sm/build.gradle @@ -109,6 +109,15 @@ tasks.named('compileTestProgramsJava') { options.annotationProcessorPath = files() } +// Error-prone must also be disabled for compileTestProgramsJava: its Gradle plugin uses +// tasks.withType(JavaCompile) and would inject -Xplugin:ErrorProne, but the +// testProgramsAnnotationProcessor configuration has no errorprone JAR on it. +plugins.withId(libs.plugins.ltgt.errorprone.get().pluginId) { + tasks.named('compileTestProgramsJava') { + options.errorprone.enabled = false + } +} + task testProgramsJar(type: Jar) { description = 'JAR of standalone violation programs used by BATS integration tests' group = 'build' diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java index d31a9417bbb5..867510b3e2d4 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java @@ -18,6 +18,7 @@ import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.URL; import java.util.Collection; import java.util.List; import java.util.Set; @@ -108,7 +109,7 @@ public void testCodebaseScopedEntryDeniedWhenCallerNotFromThatCodeBase() { @Test public void testIsCallerFromCodeBaseMatchesTestClassCodeSource() { // Get the actual code source URL of this test class and verify isCallerFromCodeBase accepts it. - java.net.URL loc = + URL loc = SocketChannelInterceptorTest.class.getProtectionDomain().getCodeSource().getLocation(); String codeBase = "file:" + loc.getPath(); // Strip any trailing "/" so it acts as an exact-directory match diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java index 69cc0ce9695e..6577fa340ccd 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java @@ -16,6 +16,8 @@ */ package org.apache.solr.security.agent; +import java.net.InetAddress; +import java.net.InetSocketAddress; import java.nio.file.Path; import java.util.List; import java.util.Set; @@ -104,8 +106,7 @@ public void testPermittedEndpointNotBlocked() { @Test(expected = SecurityException.class) public void testDeniedNetworkConnectThrowsInEnforceMode() throws Exception { buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); - java.net.InetSocketAddress addr = - new java.net.InetSocketAddress(java.net.InetAddress.getByName("10.0.0.99"), 9999); + InetSocketAddress addr = new InetSocketAddress(InetAddress.getByName("10.0.0.99"), 9999); SocketChannelInterceptor.checkConnect(addr); } @@ -113,8 +114,7 @@ public void testDeniedNetworkConnectThrowsInEnforceMode() throws Exception { public void testDeniedNetworkIncrementsCounter() throws Exception { long before = ViolationMetricsReporter.networkCount(); buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); - java.net.InetSocketAddress addr = - new java.net.InetSocketAddress(java.net.InetAddress.getByName("10.0.0.99"), 9999); + InetSocketAddress addr = new InetSocketAddress(InetAddress.getByName("10.0.0.99"), 9999); try { SocketChannelInterceptor.checkConnect(addr); } catch (SecurityException ignored) { diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java index dacedaf6da06..3cb17b4f1986 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java @@ -16,6 +16,7 @@ */ package org.apache.solr.security.agent; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -51,7 +52,7 @@ private AgentPolicy buildPolicyPermitting(Path dir) { String realDirStr; try { realDirStr = dir.toRealPath().toString(); - } catch (java.io.IOException e) { + } catch (IOException e) { realDirStr = dir.toAbsolutePath().toString(); } PermittedPath allowed = From 0b97ae9cf6124c562c6697ebb0af1e86c9f3aa25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 01:20:46 +0200 Subject: [PATCH 20/65] SOLR-17767: Policy and Docker fixes (symDir, proc/sys, cleanup user) - agent-security.policy: add ${solr.install.symDir} permission so that installations where /opt/solr is a symlink to /opt/solr-X.Y.Z/ work correctly (FileInterceptor uses toAbsolutePath, not toRealPath, so the symlink path must be explicitly permitted). - agent-security.policy: add /proc/- and /sys/- read permissions for Linux JVM internals (memory-mapping checks, network config, etc.). - PolicyPropertyExpander: resolve ${solr.install.symDir} via SOLR_TIP_SYM env var, with fallback to ${solr.install.dir}. - PolicyLoader: set trustedFileSystems = {jar, zip, jrt} so that JVM internal class-loading file reads are not intercepted (prevents spurious violations and performance overhead). - Docker test: add --user root to cleanup container so UID-8983-owned files can be deleted without AccessDeniedException. --- AGENTS.md | 1 + solr/agent-sm/build.gradle | 11 ++-- solr/agent-sm/gradle.lockfile | 2 +- .../solr/security/agent/ApprovedCallSite.java | 6 +- .../security/agent/PermittedEndpoint.java | 7 +- .../solr/security/agent/PermittedPath.java | 6 +- .../solr/security/agent/PolicyLoader.java | 11 ++-- .../agent/PolicyPropertyExpander.java | 12 +++- .../solr/security/agent/PolicySource.java | 29 ++++++++ .../agent/SecurityViolationLogger.java | 66 +++++++------------ .../security/agent/SolrAgentEntryPoint.java | 32 ++------- .../PolicyLoaderOperatorExtensionTest.java | 8 +-- .../solr/security/agent/PolicyLoaderTest.java | 14 ++-- .../agent/ProcessExecInterceptorTest.java | 6 +- .../agent/SocketChannelInterceptorTest.java | 4 +- .../agent/SolrAgentIntegrationTest.java | 6 +- .../security/agent/SymlinkEscapeTest.java | 3 +- .../agent/SystemExitInterceptorTest.java | 6 +- .../org/apache/solr/core/CoreContainer.java | 4 ++ .../solr/security/AgentViolationBridge.java | 54 +++++++++++++++ .../cases/empty-varsolr-dir-solr/test.sh | 3 +- solr/server/etc/agent-security.policy | 15 ++++- 22 files changed, 186 insertions(+), 120 deletions(-) create mode 100644 solr/agent-sm/src/java/org/apache/solr/security/agent/PolicySource.java create mode 100644 solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java diff --git a/AGENTS.md b/AGENTS.md index d53f75fe777e..f8325e396b24 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,6 +37,7 @@ While README.md and CONTRIBUTING.md are mainly written for humans, this file is - Use the project's custom `EnvUtils` to read system properties. It auto converts env.var SOLR_FOO_BAR to system property solr.foo.bar - Be careful to not add non-essential logging! If you add slf4j log calls, make sure to wrap debug/trace level calls in `logger.isXxxEnabled()` clause - Validate user input. For file paths, always call `myCoreContainer.assertPathAllowed(myPath)` before using +- Do not use Fully Qualified Class Names (FQCNs) in code unless absolutely necessary; do use imports. ## Running Tests diff --git a/solr/agent-sm/build.gradle b/solr/agent-sm/build.gradle index 7461cb7634b0..b51028a4fd42 100644 --- a/solr/agent-sm/build.gradle +++ b/solr/agent-sm/build.gradle @@ -31,8 +31,6 @@ dependencies { implementation libs.bytebuddy.agent permitUnusedDeclared libs.bytebuddy.agent - // SLF4J API for violation logging - bundled; implementation loaded via context classloader - implementation libs.slf4j.api // Test dependencies - NOT bundled testImplementation project(':solr:test-framework') @@ -50,7 +48,12 @@ dependencies { } // Build a self-contained fat JAR so the agent can run before Solr's classpath is set up. -// ByteBuddy and SLF4J API are bundled; Solr classes are NOT included. +// ByteBuddy is bundled; Solr classes are NOT included. +// +// Build a fat JAR so all agent classes (and ByteBuddy) are available on Boot-Class-Path before +// the application main class loads. SLF4J is intentionally excluded from this JAR's dependencies: +// placing SLF4J on Boot-Class-Path would initialise it before Log4j2 is loaded, permanently +// caching a NOP logger and preventing solr.log from being created. jar { manifest { attributes( @@ -62,7 +65,7 @@ jar { ) } - // Bundle runtime dependencies (ByteBuddy, SLF4J API) into the fat agent JAR. + // Bundle runtime dependencies (ByteBuddy) into the fat agent JAR. // Test dependencies are excluded because they are not on runtimeClasspath. from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } diff --git a/solr/agent-sm/gradle.lockfile b/solr/agent-sm/gradle.lockfile index 1db2dc26818e..043ae4fd3554 100644 --- a/solr/agent-sm/gradle.lockfile +++ b/solr/agent-sm/gradle.lockfile @@ -163,6 +163,6 @@ org.ow2.asm:asm:9.8=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath, org.pcollections:pcollections:4.0.1=annotationProcessor,errorprone,testAnnotationProcessor org.semver4j:semver4j:6.0.0=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy org.slf4j:jcl-over-slf4j:2.0.17=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy -org.slf4j:slf4j-api:2.0.17=compileClasspath,compileClasspathCopy,jarValidation,permitTestUnusedDeclared,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy +org.slf4j:slf4j-api:2.0.17=jarValidation,permitTestUnusedDeclared,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy org.xerial.snappy:snappy-java:1.1.10.8=jarValidation,permitTestUnusedDeclared,testRuntimeClasspath,testRuntimeClasspathCopy empty=apiHelper,apiHelperTest,apiHelperTestPrograms,compileOnlyHelperTest,compileOnlyHelperTestPrograms,missingdoclet,permitAggregatorUse,permitTestAggregatorUse,permitTestProgramsAggregatorUse,permitTestProgramsUnusedDeclared,permitTestProgramsUsedUndeclared,permitTestUsedUndeclared,permitUsedUndeclared,testProgramsAnnotationProcessor,testProgramsCompileClasspath,testProgramsRuntimeClasspath diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java index bfbc20f88ffd..37d8b33a4aee 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java @@ -43,9 +43,9 @@ public enum Operation { private final String classNamePattern; private final Operation operation; - private final PolicyLoader.PolicySource source; + private final PolicySource source; - ApprovedCallSite(String classNamePattern, Operation operation, PolicyLoader.PolicySource source) { + ApprovedCallSite(String classNamePattern, Operation operation, PolicySource source) { this.classNamePattern = classNamePattern; this.operation = operation; this.source = source; @@ -62,7 +62,7 @@ public Operation operation() { } /** Whether this entry came from the default bundled policy or an operator extension. */ - public PolicyLoader.PolicySource source() { + public PolicySource source() { return source; } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedEndpoint.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedEndpoint.java index 08a04f20a383..8d385b28fb8a 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedEndpoint.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedEndpoint.java @@ -32,10 +32,9 @@ public final class PermittedEndpoint { private final String hostPort; private final String actions; private final String codeBase; // null → applies to all code - private final PolicyLoader.PolicySource source; + private final PolicySource source; - PermittedEndpoint( - String hostPort, String actions, String codeBase, PolicyLoader.PolicySource source) { + PermittedEndpoint(String hostPort, String actions, String codeBase, PolicySource source) { this.hostPort = hostPort; this.actions = actions != null ? actions : "connect,resolve"; this.codeBase = codeBase; @@ -65,7 +64,7 @@ public String codeBase() { } /** Whether this rule came from the default bundled policy or an operator extension. */ - public PolicyLoader.PolicySource source() { + public PolicySource source() { return source; } } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java index e03fcb2303fb..0173b320cebd 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java @@ -31,9 +31,9 @@ public final class PermittedPath { private final String path; private final String actions; private final boolean recursive; - private final PolicyLoader.PolicySource source; + private final PolicySource source; - PermittedPath(String path, String actions, boolean recursive, PolicyLoader.PolicySource source) { + PermittedPath(String path, String actions, boolean recursive, PolicySource source) { this.path = path; this.actions = actions != null ? actions.toLowerCase(Locale.ROOT) : "read"; this.recursive = recursive; @@ -56,7 +56,7 @@ public boolean recursive() { } /** Whether this rule came from the default bundled policy or an operator extension. */ - public PolicyLoader.PolicySource source() { + public PolicySource source() { return source; } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java index ac548349178c..db34bc4bff8d 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java @@ -49,12 +49,6 @@ */ public class PolicyLoader { - /** The source that an entry came from — default bundled policy or operator extension. */ - public enum PolicySource { - DEFAULT, - OPERATOR - } - /** * Loads and merges the default policy file and the optional operator extension file. * @@ -231,8 +225,11 @@ private AgentPolicy buildPolicy(List grants) { // 0.0.0.0 is the unspecified bind address, not a loopback — intentionally excluded. Set trustedHosts = Set.of("localhost", "127.0.0.1", "0:0:0:0:0:0:0:1", "::1"); + // jar/zip/jrt are internal JVM class-loading file systems. Intercepting them causes a + // class-initialization deadlock: violation logger → SLF4J → Log4j2 init → JAR read → repeat. + Set trustedFileSystems = Set.of("jar", "zip", "jrt"); return new AgentPolicy( - paths, endpoints, exitCallers, execCallers, mode, Set.of(), trustedHosts); + paths, endpoints, exitCallers, execCallers, mode, trustedFileSystems, trustedHosts); } // --------------------------------------------------------------------------- diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyPropertyExpander.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyPropertyExpander.java index 7862f49662fd..8e59727fd517 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyPropertyExpander.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyPropertyExpander.java @@ -64,7 +64,8 @@ static class ExpandException extends Exception { private static final Map CUSTOM_ENV_NAMES = Map.of( "solr.solr.home", "SOLR_HOME", - "solr.install.dir", "SOLR_TIP"); + "solr.install.dir", "SOLR_TIP", + "solr.install.symDir", "SOLR_TIP_SYM"); /** * Returns the value for {@code sysprop} by checking, in order: @@ -128,6 +129,15 @@ private static String expandPlaceholder(String placeholder) throws ExpandExcepti return "9983"; } } + // solr.install.symDir is optional — falls back to solr.install.dir when absent. + if ("solr.install.symDir".equals(placeholder)) { + String val = getPropertyOrEnv("solr.install.symDir"); + if (val != null) return val; + val = getPropertyOrEnv("solr.install.dir"); + if (val != null) return val; + throw new ExpandException( + "Unresolved policy variable: ${solr.install.symDir} (and ${solr.install.dir} is also unset)"); + } // solr.data.home is optional — when not configured, Solr stores data under solr.solr.home if ("solr.data.home".equals(placeholder)) { String val = getPropertyOrEnv("solr.data.home"); diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicySource.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicySource.java new file mode 100644 index 000000000000..1566f098ad66 --- /dev/null +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicySource.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +/** + * Whether a security policy entry came from the default bundled policy ({@code + * agent-security.policy}) or an operator extension. Standalone top-level type (not nested in {@link + * PolicyLoader}) because {@link PermittedPath}, {@link PermittedEndpoint}, and {@link + * ApprovedCallSite} hold it as a field and are injected into bootstrap — a nested type would force + * {@link PolicyLoader} into bootstrap too. + */ +public enum PolicySource { + DEFAULT, + OPERATOR +} diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java index d020bacf9e97..d3677ca296ed 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java @@ -16,9 +16,7 @@ */ package org.apache.solr.security.agent; -import java.lang.invoke.MethodHandles; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.function.Consumer; /** * Emits structured log entries for security policy violations detected by the Solr security agent. @@ -26,26 +24,25 @@ *

      Log format

      * *
      {@code
      - * SECURITY VIOLATION [TYPE] target= caller= mode= source=
      + * [Solr SecurityAgent] SECURITY VIOLATION [TYPE] target= caller= mode= source=
        * }
      * - *

      In {@link AgentPolicy.EnforcementMode#WARN warn mode} entries are logged at {@code WARN} level - * and the operation is allowed to proceed. In {@link AgentPolicy.EnforcementMode#ENFORCE enforce - * mode} entries are logged at {@code ERROR} level and the operation must be blocked by the calling - * interceptor. + *

      In {@link AgentPolicy.EnforcementMode#WARN warn mode} entries are emitted and the operation + * proceeds. In {@link AgentPolicy.EnforcementMode#ENFORCE enforce mode} the operation is blocked. * - *

      The {@code source} field identifies whether the matching policy entry (if any) came from the - * default bundled policy ({@code DEFAULT}) or from an operator extension ({@code OPERATOR}). For - * violations where no entry matched at all, {@code source} is omitted. - * - *

      SLF4J and classloader boundary

      - * - * Agent classes may be injected into the bootstrap classloader, where SLF4J is not directly - * accessible. The logger is obtained lazily via the context classloader to bridge this boundary. If - * SLF4J is not yet available (early startup), violations are written to {@code System.err}. + *

      During early startup violations go to {@code System.err}. Once {@code + * AgentViolationBridge.wire()} is called from {@code CoreContainer}, the {@link #reporter} bridge + * routes them to {@code solr.log} at {@code WARN} level. */ public final class SecurityViolationLogger { + /** + * Bridge set by {@code AgentViolationBridge.wire()} once SLF4J/Log4j2 is available. {@code null} + * until wired — violations fall back to {@code System.err}. {@code volatile} for safe + * publication. + */ + public static volatile Consumer reporter = null; + /** The operation types that can trigger a violation. */ public enum ViolationType { FILE_READ, @@ -56,20 +53,13 @@ public enum ViolationType { PROCESS_EXEC } - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private SecurityViolationLogger() {} - /** - * Records a security violation. - * - * @param type the category of the blocked/warned operation - * @param target the resource that was targeted (path, host:port, or operation descriptor) - * @param caller the fully-qualified name of the top non-JDK caller class - * @param mode the current enforcement mode - * @param source the policy source tag ({@code "DEFAULT"}, {@code "OPERATOR"}, or {@code null} if - * no entry matched) - */ + /** Records a security violation. */ + @SuppressForbidden( + reason = + "System.err is the only output channel safe from classloader conflicts in a bootstrap " + + "agent. SLF4J in Boot-Class-Path permanently poisons the JVM-wide SLF4J binding.") public static void log( ViolationType type, String target, @@ -78,23 +68,15 @@ public static void log( String source) { String message = buildMessage(type, target, caller, mode, source); - - if (mode == AgentPolicy.EnforcementMode.ENFORCE) { - log.error(message); + Consumer r = reporter; + if (r != null) { + r.accept(message); } else { - log.warn(message); - } - // When DEBUG logging is enabled, emit the current call stack so the violation origin is visible - // in logs. The RuntimeException is never thrown — it is a carrier for the stack trace only. - if (log.isDebugEnabled()) { - log.debug("Call stack at point of violation:", new RuntimeException("call stack")); + System.err.println("[Solr SecurityAgent] " + message); } } - /** - * Convenience overload without a {@code source} field (used during early startup before policy - * source tagging is available). - */ + /** Convenience overload without a {@code source} field (used before policy source tagging). */ public static void log( ViolationType type, String target, String caller, AgentPolicy.EnforcementMode mode) { log(type, target, caller, mode, null); diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java index 788a656eb033..8e3a791f5a21 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java @@ -47,12 +47,11 @@ * WARN} mode, log the error and continue without protection. *

    * - *

    Bootstrap injection

    + *

    Bootstrap classloader

    * - * The interceptor classes ({@link FileInterceptor}, etc.) are injected into the bootstrap - * classloader using {@code ClassInjector.UsingUnsafe.ofBootLoader()} so that they can intercept JDK - * methods which are loaded by the bootstrap loader. The {@code @SuppressForbidden} annotation on - * the injection call acknowledges the intentional use of {@code sun.misc.Unsafe}. + * The agent JAR is placed on {@code Boot-Class-Path} so all interceptor and policy classes are + * visible to the bootstrap classloader. SLF4J is intentionally absent from the fat JAR to avoid + * poisoning the SLF4J binding before Log4j2 is loaded. */ public final class SolrAgentEntryPoint { @@ -61,21 +60,12 @@ private SolrAgentEntryPoint() {} /** * Called by the JVM before the application main class is loaded, when the agent JAR is specified * via {@code -javaagent:}. - * - * @param agentArgs optional agent argument string (unused) - * @param inst the {@link Instrumentation} instance provided by the JVM */ public static void premain(String agentArgs, Instrumentation inst) { bootAgent(inst); } - /** - * Called by the JVM when the agent is attached dynamically (post-startup). Delegates to {@link - * #premain(String, Instrumentation)}. - * - * @param agentArgs optional agent argument string (unused) - * @param inst the {@link Instrumentation} instance provided by the JVM - */ + /** Called by the JVM when the agent is attached dynamically; delegates to {@link #premain}. */ public static void agentmain(String agentArgs, Instrumentation inst) { premain(agentArgs, inst); } @@ -86,9 +76,8 @@ public static void agentmain(String agentArgs, Instrumentation inst) { @SuppressForbidden( reason = - "System.err is the only output available during premain (before logging is initialized). " - + "System.exit(1) is required to halt the JVM on a fatal policy-load failure in " - + "enforce mode — this is intentional agent behavior, not application code.") + "System.err is the only safe output during premain; System.exit(1) is required to halt " + + "the JVM on fatal policy-load failure in enforce mode.") private static void bootAgent(Instrumentation inst) { // Locate the default policy file next to the agent JAR. Path defaultPolicyPath = resolveDefaultPolicyPath(); @@ -98,7 +87,6 @@ private static void bootAgent(Instrumentation inst) { PolicyLoader loader = new PolicyLoader(); policy = loader.load(defaultPolicyPath); } catch (IllegalStateException e) { - // Policy load failed. String modeStr = PolicyPropertyExpander.getPropertyOrEnv("solr.security.agent.mode"); if (modeStr == null) modeStr = "warn"; if ("enforce".equalsIgnoreCase(modeStr.trim())) { @@ -118,7 +106,6 @@ private static void bootAgent(Instrumentation inst) { AgentPolicy.initialize(policy); - // Register ByteBuddy interceptors. try { installInterceptors(inst); } catch (Exception e) { @@ -148,11 +135,6 @@ private static void bootAgent(Instrumentation inst) { "open" }; - /** - * Installs all ByteBuddy interceptors using the provided {@link Instrumentation} instance. The - * interceptor classes are injected into the bootstrap classloader so that they can redefine JDK - * methods. - */ private static void installInterceptors(Instrumentation inst) { // AgentBuilder configuration for JDK class instrumentation: // - Implementation.Context.Disabled: required for REDEFINE so ByteBuddy does not try to diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java index 95109cfa22ae..a6ac5864a40e 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java @@ -32,7 +32,7 @@ *
      *
    • An extra policy file present at the configured path is loaded and merged with the default * policy. - *
    • Entries from the extra policy are tagged {@link PolicyLoader.PolicySource#OPERATOR}. + *
    • Entries from the extra policy are tagged {@link PolicySource#OPERATOR}. *
    • Paths listed only in the extra policy are permitted; unlisted paths remain blocked. *
    • When the extra policy file is absent the default policy still loads normally. *
    • A malformed extra policy causes {@link IllegalStateException} with a descriptive message. @@ -127,8 +127,7 @@ public void testExtraPolicyEntriesTaggedOperator() throws Exception { AgentPolicy policy = loadWithExtra(defaultPolicy, extraPolicy); List paths = policy.permittedPaths(); - boolean hasOperator = - paths.stream().anyMatch(p -> p.source() == PolicyLoader.PolicySource.OPERATOR); + boolean hasOperator = paths.stream().anyMatch(p -> p.source() == PolicySource.OPERATOR); assertTrue("Expected at least one OPERATOR-sourced path entry", hasOperator); } @@ -139,8 +138,7 @@ public void testDefaultPolicyEntriesTaggedDefault() throws Exception { AgentPolicy policy = loadWithExtra(defaultPolicy, null); List paths = policy.permittedPaths(); - boolean hasDefault = - paths.stream().anyMatch(p -> p.source() == PolicyLoader.PolicySource.DEFAULT); + boolean hasDefault = paths.stream().anyMatch(p -> p.source() == PolicySource.DEFAULT); assertTrue("Expected at least one DEFAULT-sourced path entry", hasDefault); } diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java index 39f823ba0512..5bf6fa8b6268 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java @@ -79,7 +79,7 @@ public void testGlobalGrantFilePermissionParsed() { String policy = "grant {\n" + " permission java.io.FilePermission \"/solr/home/-\", \"read\";\n" + "};"; List blocks = new ArrayList<>(); - PolicyLoader.parsePolicyBlocks(policy, PolicyLoader.PolicySource.DEFAULT, blocks); + PolicyLoader.parsePolicyBlocks(policy, PolicySource.DEFAULT, blocks); assertEquals(1, blocks.size()); PolicyLoader.GrantBlock block = blocks.get(0); @@ -87,7 +87,7 @@ public void testGlobalGrantFilePermissionParsed() { assertEquals(1, block.filePaths.size()); assertEquals("/solr/home/-", block.filePaths.get(0).target); assertEquals("read", block.filePaths.get(0).actions); - assertEquals(PolicyLoader.PolicySource.DEFAULT, block.filePaths.get(0).source); + assertEquals(PolicySource.DEFAULT, block.filePaths.get(0).source); } @Test @@ -97,7 +97,7 @@ public void testGlobalGrantSocketPermissionParsed() { + " permission java.net.SocketPermission \"*:8983\", \"connect,resolve\";\n" + "};"; List blocks = new ArrayList<>(); - PolicyLoader.parsePolicyBlocks(policy, PolicyLoader.PolicySource.DEFAULT, blocks); + PolicyLoader.parsePolicyBlocks(policy, PolicySource.DEFAULT, blocks); assertEquals(1, blocks.size()); assertEquals(1, blocks.get(0).socketPerms.size()); @@ -112,7 +112,7 @@ public void testCodeBaseScopedGrantParsed() { + " permission java.net.SocketPermission \"*\", \"connect,resolve\";\n" + "};"; List blocks = new ArrayList<>(); - PolicyLoader.parsePolicyBlocks(policy, PolicyLoader.PolicySource.DEFAULT, blocks); + PolicyLoader.parsePolicyBlocks(policy, PolicySource.DEFAULT, blocks); assertEquals(1, blocks.size()); PolicyLoader.GrantBlock block = blocks.get(0); @@ -130,7 +130,7 @@ public void testLineCommentsStripped() { + " permission java.io.FilePermission \"/tmp/-\", \"read,write,delete\";\n" + "};"; List blocks = new ArrayList<>(); - PolicyLoader.parsePolicyBlocks(policy, PolicyLoader.PolicySource.DEFAULT, blocks); + PolicyLoader.parsePolicyBlocks(policy, PolicySource.DEFAULT, blocks); assertEquals(1, blocks.size()); assertEquals(1, blocks.get(0).filePaths.size()); @@ -144,7 +144,7 @@ public void testUnrecognisedPermissionTypeIgnored() { + " permission java.io.FilePermission \"/tmp/-\", \"read\";\n" + "};"; List blocks = new ArrayList<>(); - PolicyLoader.parsePolicyBlocks(policy, PolicyLoader.PolicySource.DEFAULT, blocks); + PolicyLoader.parsePolicyBlocks(policy, PolicySource.DEFAULT, blocks); assertEquals(1, blocks.size()); assertEquals(1, blocks.get(0).filePaths.size()); @@ -225,7 +225,7 @@ public void testExtraPolicyMergedAndTaggedOperator() throws Exception { // Operator path carries OPERATOR source tag PermittedPath operatorPath = paths.stream().filter(p -> p.path().equals("/mnt/nfs")).findFirst().orElseThrow(); - assertEquals(PolicyLoader.PolicySource.OPERATOR, operatorPath.source()); + assertEquals(PolicySource.OPERATOR, operatorPath.source()); } finally { System.clearProperty("solr.security.agent.extra.policy"); } diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java index 4c18c5d57196..033984535390 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/ProcessExecInterceptorTest.java @@ -45,9 +45,7 @@ private void initPolicy(boolean approved, String callerClass, AgentPolicy.Enforc approved ? List.of( new ApprovedCallSite( - callerClass, - ApprovedCallSite.Operation.EXEC, - PolicyLoader.PolicySource.DEFAULT)) + callerClass, ApprovedCallSite.Operation.EXEC, PolicySource.DEFAULT)) : List.of(); AgentPolicy policy = new AgentPolicy(List.of(), List.of(), List.of(), execCallers, mode); AgentPolicy.initialize(policy); @@ -111,7 +109,7 @@ public void testApprovedCallerAnywhereInChainPermits() { new ApprovedCallSite( ProcessExecInterceptorTest.class.getName(), ApprovedCallSite.Operation.EXEC, - PolicyLoader.PolicySource.DEFAULT)); + PolicySource.DEFAULT)); AgentPolicy policy = new AgentPolicy( List.of(), List.of(), List.of(), execCallers, AgentPolicy.EnforcementMode.ENFORCE); diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java index 867510b3e2d4..03cf18168eef 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java @@ -36,7 +36,7 @@ public void resetSingleton() { private AgentPolicy policyWithEndpoint(String hostPort) { PermittedEndpoint ep = - new PermittedEndpoint(hostPort, "connect,resolve", null, PolicyLoader.PolicySource.DEFAULT); + new PermittedEndpoint(hostPort, "connect,resolve", null, PolicySource.DEFAULT); return new AgentPolicy( List.of(), List.of(ep), List.of(), List.of(), AgentPolicy.EnforcementMode.ENFORCE); } @@ -95,7 +95,7 @@ public void testCodebaseScopedEntryDeniedWhenCallerNotFromThatCodeBase() { "*", "connect,resolve", "file:/nonexistent/solr/modules/jwt-auth/-", - PolicyLoader.PolicySource.DEFAULT); + PolicySource.DEFAULT); AgentPolicy policy = new AgentPolicy( List.of(), diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java index 6577fa340ccd..b68cf48bb0f3 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java @@ -60,7 +60,7 @@ private AgentPolicy buildEnforcePolicy( public void testPermittedFileReadSucceeds() { Path tmpDir = createTempDir(); PermittedPath allowed = - new PermittedPath(tmpDir.toString(), "read", true, PolicyLoader.PolicySource.DEFAULT); + new PermittedPath(tmpDir.toString(), "read", true, PolicySource.DEFAULT); buildEnforcePolicy(List.of(allowed), List.of(), List.of(), List.of()); Path target = tmpDir.resolve("test.txt"); @@ -97,7 +97,7 @@ public void testDeniedFileReadIncrementsFileCounter() { @Test public void testPermittedEndpointNotBlocked() { PermittedEndpoint ep = - new PermittedEndpoint("*:8983", "connect,resolve", null, PolicyLoader.PolicySource.DEFAULT); + new PermittedEndpoint("*:8983", "connect,resolve", null, PolicySource.DEFAULT); buildEnforcePolicy(List.of(), List.of(ep), List.of(), List.of()); assertTrue( SocketChannelInterceptor.isEndpointPermitted(AgentPolicy.getInstance(), "10.0.0.1", 8983)); @@ -140,7 +140,7 @@ public void testApprovedExitChainPermitted() { new ApprovedCallSite( SolrAgentIntegrationTest.class.getName(), ApprovedCallSite.Operation.EXIT, - PolicyLoader.PolicySource.DEFAULT)); + PolicySource.DEFAULT)); buildEnforcePolicy(List.of(), List.of(), exitCallers, List.of()); assertTrue( AgentPolicy.getInstance().isChainThatCanExit(Set.of(SolrAgentIntegrationTest.class))); diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java index 3cb17b4f1986..aee886c0878d 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java @@ -55,8 +55,7 @@ private AgentPolicy buildPolicyPermitting(Path dir) { } catch (IOException e) { realDirStr = dir.toAbsolutePath().toString(); } - PermittedPath allowed = - new PermittedPath(realDirStr, "read", true, PolicyLoader.PolicySource.DEFAULT); + PermittedPath allowed = new PermittedPath(realDirStr, "read", true, PolicySource.DEFAULT); AgentPolicy policy = new AgentPolicy( List.of(allowed), List.of(), List.of(), List.of(), AgentPolicy.EnforcementMode.ENFORCE); diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SystemExitInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SystemExitInterceptorTest.java index 7e130ba3e442..a5215702ad3d 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SystemExitInterceptorTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SystemExitInterceptorTest.java @@ -42,9 +42,7 @@ private AgentPolicy initPolicy( approved ? List.of( new ApprovedCallSite( - callerClass, - ApprovedCallSite.Operation.EXIT, - PolicyLoader.PolicySource.DEFAULT)) + callerClass, ApprovedCallSite.Operation.EXIT, PolicySource.DEFAULT)) : List.of(); AgentPolicy policy = new AgentPolicy(List.of(), List.of(), exitCallers, List.of(), mode); AgentPolicy.initialize(policy); @@ -86,7 +84,7 @@ public void testWildcardPatternMatchesPackage() { new ApprovedCallSite( "org.apache.solr.security.agent.*", ApprovedCallSite.Operation.EXIT, - PolicyLoader.PolicySource.DEFAULT)); + PolicySource.DEFAULT)); AgentPolicy policy = new AgentPolicy( List.of(), List.of(), exitCallers, List.of(), AgentPolicy.EnforcementMode.ENFORCE); diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java index 42ea6c6a25fe..418f4bfdfb86 100644 --- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java +++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java @@ -145,6 +145,7 @@ import org.apache.solr.search.SolrCache; import org.apache.solr.search.SolrFieldCacheBean; import org.apache.solr.search.SolrIndexSearcher; +import org.apache.solr.security.AgentViolationBridge; import org.apache.solr.security.AllowListUrlChecker; import org.apache.solr.security.AuditEvent; import org.apache.solr.security.AuditLoggerPlugin; @@ -969,6 +970,9 @@ private void loadInternal() { log.warn("Failed to register security agent metrics", e); } + // Wire security agent violations to SLF4J; no-op if agent JAR is absent. + AgentViolationBridge.wire(); + // setup executor to load cores in parallel coreLoadExecutor = solrMetricsContext.instrumentedExecutorService( diff --git a/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java b/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java new file mode 100644 index 000000000000..07548a509e7c --- /dev/null +++ b/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security; + +import java.lang.invoke.MethodHandles; +import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wires the Solr security agent's violation reporter to SLF4J once Log4j2 is available. Uses + * reflection because {@code solr:agent-sm} and {@code solr:core} have no compile-time dependency on + * each other. Call {@link #wire()} from {@code CoreContainer} after Log4j2 is initialised; from + * that point violations appear in {@code solr.log} rather than {@code System.err}. Safe to call + * when the agent JAR is absent — {@link ClassNotFoundException} is silently ignored. + */ +public final class AgentViolationBridge { + + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private AgentViolationBridge() {} + + /** + * Wires the security agent violation reporter to SLF4J. No-op if the agent JAR is not present. + */ + public static void wire() { + try { + // SecurityViolationLogger is injected into the bootstrap classloader by SolrAgentEntryPoint. + Class cls = Class.forName("org.apache.solr.security.agent.SecurityViolationLogger"); + java.lang.reflect.Field f = cls.getField("reporter"); + Consumer bridge = msg -> log.warn("SECURITY VIOLATION {}", msg); + f.set(null, bridge); + log.info("Security agent violation reporter wired to SLF4J"); + } catch (ClassNotFoundException e) { + // Agent JAR not loaded — security agent is inactive, nothing to wire + } catch (Exception e) { + log.warn("Could not wire security agent violation reporter to SLF4J", e); + } + } +} diff --git a/solr/docker/tests/cases/empty-varsolr-dir-solr/test.sh b/solr/docker/tests/cases/empty-varsolr-dir-solr/test.sh index 1408ad4d704d..b26f451c7696 100755 --- a/solr/docker/tests/cases/empty-varsolr-dir-solr/test.sh +++ b/solr/docker/tests/cases/empty-varsolr-dir-solr/test.sh @@ -46,8 +46,9 @@ container_cleanup "$container_name" ls -l "$myvarsolr"/ -# remove the solr-owned files from inside a container +# remove the solr-owned files from inside a container (as root so UID 8983-owned dirs are deletable) docker run --rm -e VERBOSE=yes \ + --user root \ -v "$myvarsolr:/myvarsolr" "$tag" \ bash -c "rm -fr /myvarsolr/*" diff --git a/solr/server/etc/agent-security.policy b/solr/server/etc/agent-security.policy index 3c1aae44339b..9b0e583b52ef 100644 --- a/solr/server/etc/agent-security.policy +++ b/solr/server/etc/agent-security.policy @@ -24,7 +24,8 @@ // // Syntax: JDK-style .policy files with Solr variable substitution. // Supported variables: ${solr.solr.home}, ${solr.data.home}, ${solr.logs.dir}, ${solr.install.dir}, -// ${java.io.tmpdir}, ${java.home}, ${solr.port.listen}, ${solr.zk.port} +// ${solr.install.symDir}, ${java.io.tmpdir}, ${java.home}, +// ${solr.port.listen}, ${solr.zk.port} // // For documentation see the Solr Reference Guide: Security Agent. @@ -36,7 +37,11 @@ grant { // --- File system access --- // Solr installation directory: read-only (JARs, modules, Jetty config, etc.) - permission java.io.FilePermission "${solr.install.dir}/-", "read"; + // Both the real path and the symlinked path are permitted so that installations where + // /opt/solr -> /opt/solr-X.Y.Z/ work correctly (FileInterceptor uses toAbsolutePath(), + // not toRealPath(), so accesses via the symlink path must be explicitly allowed). + permission java.io.FilePermission "${solr.install.dir}/-", "read"; + permission java.io.FilePermission "${solr.install.symDir}/-", "read"; // Solr home: read-only (config files, schema, solrconfig.xml, etc.) permission java.io.FilePermission "${solr.solr.home}/-", "read"; @@ -53,6 +58,12 @@ grant { // JDK runtime libraries: read-only (class loading, native libs, etc.) permission java.io.FilePermission "${java.home}/-", "read"; + // Linux pseudo-filesystems: read-only access for JVM internals (e.g. memory-mapping checks, + // network configuration, kernel parameters). These paths do not exist on macOS/Windows and + // their presence in a cross-platform policy is harmless. + permission java.io.FilePermission "/proc/-", "read"; + permission java.io.FilePermission "/sys/-", "read"; + // --- Outbound network access --- // Loopback addresses: unconditionally permitted (inter-thread, localhost HTTP, embedded ZK) From c33ad1e015c405894166cf40e05530e652fbf300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 02:08:47 +0200 Subject: [PATCH 21/65] Fix UnnecessarilyFullyQualified: import Field in AgentViolationBridge --- .../java/org/apache/solr/security/AgentViolationBridge.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java b/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java index 07548a509e7c..766c1195f052 100644 --- a/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java +++ b/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java @@ -17,6 +17,7 @@ package org.apache.solr.security; import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,9 +40,9 @@ private AgentViolationBridge() {} */ public static void wire() { try { - // SecurityViolationLogger is injected into the bootstrap classloader by SolrAgentEntryPoint. + // SecurityViolationLogger is in the bootstrap classloader via Boot-Class-Path. Class cls = Class.forName("org.apache.solr.security.agent.SecurityViolationLogger"); - java.lang.reflect.Field f = cls.getField("reporter"); + Field f = cls.getField("reporter"); Consumer bridge = msg -> log.warn("SECURITY VIOLATION {}", msg); f.set(null, bridge); log.info("Security agent violation reporter wired to SLF4J"); From 31d62cd86b2db967504d8ac7e8f0c582c77274e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 10:13:56 +0200 Subject: [PATCH 22/65] args[1] bug Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../java/org/apache/solr/security/agent/FileInterceptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java index 5e43d1d66939..f0149d36ade9 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java @@ -95,7 +95,7 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin if (isMutating == false && isDelete == false) { if (name.equals("newByteChannel") == true || name.equals("open") == true) { if (args.length > 1) { - if (args instanceof OpenOption[] opts) { + if (args[1] instanceof OpenOption[] opts) { for (final OpenOption opt : opts) { if (opt != StandardOpenOption.READ) { isMutating = true; From e3f35058448a7e59ed1894e6f3e8cf4131e3fa3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 10:08:54 +0200 Subject: [PATCH 23/65] Fix wrong JIRA in changelog file --- .../{SOLR-15868-java-agent.yml => SOLR-17767-java-agent.yml} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename changelog/unreleased/{SOLR-15868-java-agent.yml => SOLR-17767-java-agent.yml} (73%) diff --git a/changelog/unreleased/SOLR-15868-java-agent.yml b/changelog/unreleased/SOLR-17767-java-agent.yml similarity index 73% rename from changelog/unreleased/SOLR-15868-java-agent.yml rename to changelog/unreleased/SOLR-17767-java-agent.yml index a6f6a991c4b6..f8985936abac 100644 --- a/changelog/unreleased/SOLR-15868-java-agent.yml +++ b/changelog/unreleased/SOLR-17767-java-agent.yml @@ -5,5 +5,5 @@ type: added authors: - name: Jan Høydahl links: - - name: SOLR-15868 - url: https://issues.apache.org/jira/browse/SOLR-15868 + - name: SOLR-17767 + url: https://issues.apache.org/jira/browse/SOLR-17767 From 1601f319c7baad4e13dfbe367cbbc8a17f26c1fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 10:16:09 +0200 Subject: [PATCH 24/65] Fix security-agent.adoc: correct policy variable names in docs table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The variable names must match what PolicyPropertyExpander recognises: solr.home → solr.solr.home, solr.data.dir → solr.data.home, solr.log.dir → solr.logs.dir, solr.port → solr.port.listen. Also added solr.install.symDir row and corrected the intra-cluster network policy example. --- .../deployment-guide/pages/security-agent.adoc | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc index 62757fecd073..098215acb2c4 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc @@ -69,19 +69,20 @@ Any java property will be expanded in policy entries, here are the most common. [cols="1,2",options="header"] |=== |Variable|Resolved Value -|`${solr.home}`|Solr home directory -|`${solr.data.dir}`|Solr data directory -|`${solr.log.dir}`|Solr log directory +|`${solr.solr.home}`|Solr home directory +|`${solr.data.home}`|Solr data directory (defaults to `solr.solr.home` if unset) +|`${solr.logs.dir}`|Solr log directory |`${solr.install.dir}`|Solr installation root (parent of `server/`) +|`${solr.install.symDir}`|Symlink-resolved installation root (falls back to `solr.install.dir`) |`${java.io.tmpdir}`|JVM temporary directory |`${java.home}`|JDK installation directory -|`${solr.port}`|Solr HTTP port -|`${solr.zk.port}`|ZooKeeper port (defaults to `solr.port + 1000` for embedded ZK) +|`${solr.port.listen}`|Solr HTTP port +|`${solr.zk.port}`|ZooKeeper port (defaults to `solr.port.listen + 1000` for embedded ZK) |=== === Default Intra-Cluster Network Policy -The bundled policy permits connections on `*:${solr.port}` and `*:${solr.zk.port}` from any host, so all cluster nodes can communicate without any operator action. +The bundled policy permits connections on `*:${solr.port.listen}` and `*:${solr.zk.port}` from any host, so all cluster nodes can communicate without any operator action. If your ZooKeeper runs on a non-standard port, set `-Dsolr.zk.port=` at startup or configure it explicitly in the policy. === Adding Custom Policy Entries From 23f08e29a52b0cbdfb570e040900e0795659d2a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 10:16:48 +0200 Subject: [PATCH 25/65] Fix agent-security-extra.policy: correct supported variable names in comment --- solr/server/etc/agent-security-extra.policy | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/solr/server/etc/agent-security-extra.policy b/solr/server/etc/agent-security-extra.policy index c0a77643836c..ba512e144139 100644 --- a/solr/server/etc/agent-security-extra.policy +++ b/solr/server/etc/agent-security-extra.policy @@ -28,8 +28,10 @@ // An absent file is silently skipped — it is not a startup error. // // Syntax: JDK-style .policy files (same as agent-security.policy). -// Supported variables: ${solr.home}, ${solr.data.dir}, ${solr.log.dir}, ${solr.install.dir}, -// ${java.io.tmpdir}, ${java.home}, ${solr.port}, ${solr.zk.port} +// Supported variables: ${solr.solr.home}, ${solr.data.home}, ${solr.logs.dir}, +// ${solr.install.dir}, ${solr.install.symDir}, +// ${java.io.tmpdir}, ${java.home}, +// ${solr.port.listen}, ${solr.zk.port} // // For documentation and examples, see the Solr Reference Guide: Security Agent. // From ab04af67e9c01f6facac5af94365a4b9ea89c9cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 10:17:20 +0200 Subject: [PATCH 26/65] Fix security-agent.adoc: clarify env-var mapping direction in variable substitution --- .../modules/deployment-guide/pages/security-agent.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc index 098215acb2c4..4f642e0eff9e 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc @@ -64,7 +64,7 @@ Its location can be overridden via `SOLR_SECURITY_AGENT_EXTRA_POLICY`. === Variable Substitution -Any java property will be expanded in policy entries, here are the most common. Environment variables are expanded if they adhere to the mapping convention `SOLR_FOO --> solr.foo`: +Any system property will be expanded in policy entries. Each property is also looked up as an environment variable by upper-casing and replacing `.` with `_` (e.g. `solr.solr.home` → `SOLR_SOLR_HOME`). The most common variables are: [cols="1,2",options="header"] |=== From e76205603fa34485eca904816a0886ea22f91adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 10:17:50 +0200 Subject: [PATCH 27/65] =?UTF-8?q?Fix=20typo=20in=20security-agent.adoc:=20?= =?UTF-8?q?"of=20if"=20=E2=86=92=20"or=20if"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/deployment-guide/pages/security-agent.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc index 4f642e0eff9e..3c564193cbd5 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc @@ -145,7 +145,7 @@ SOLR_SECURITY_AGENT_SKIP=true [WARNING] ==== Disabling the agent removes all runtime security controls. -Use this only for temporary troubleshooting of if you intend to provide similar protection on the operating system level. +Use this only for temporary troubleshooting or if you intend to provide similar protection on the operating system level. ==== == Modules with Pre-Permitted Network Access From ce1bbaceea674d11309a2ed577c4275eaddbf7ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 10:19:39 +0200 Subject: [PATCH 28/65] Combine Runtime.class transform chains into one to avoid duplicate redefinition --- .../solr/security/agent/SolrAgentEntryPoint.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java index 8e3a791f5a21..67069a4da0b6 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java @@ -184,24 +184,22 @@ private static void installInterceptors(Instrumentation inst) { (builder, type, classLoader, module, domain) -> builder.visit( Advice.to(SystemExitInterceptor.class).on(ElementMatchers.named("exit")))) - // Intercept Runtime.halt(int) → RuntimeHaltInterceptor + // Intercept Runtime.halt(int) and Runtime.exec(...) in a single chain to avoid + // applying two separate redefinitions to the same class. .type(ElementMatchers.is(Runtime.class)) .transform( (builder, type, classLoader, module, domain) -> - builder.visit( - Advice.to(RuntimeHaltInterceptor.class).on(ElementMatchers.named("halt")))) + builder + .visit( + Advice.to(RuntimeHaltInterceptor.class).on(ElementMatchers.named("halt"))) + .visit( + Advice.to(ProcessExecInterceptor.class).on(ElementMatchers.named("exec")))) // Intercept ProcessBuilder.start() → ProcessExecInterceptor .type(ElementMatchers.is(ProcessBuilder.class)) .transform( (builder, type, classLoader, module, domain) -> builder.visit( Advice.to(ProcessExecInterceptor.class).on(ElementMatchers.named("start")))) - // Intercept Runtime.exec(String[]) → ProcessExecInterceptor - .type(ElementMatchers.is(Runtime.class)) - .transform( - (builder, type, classLoader, module, domain) -> - builder.visit( - Advice.to(ProcessExecInterceptor.class).on(ElementMatchers.named("exec")))) .installOn(inst); } From 754c9481f1d45aec65df3f547c4a720cdaeefb16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 10:21:16 +0200 Subject: [PATCH 29/65] Use setReporter() method instead of reflective field write in AgentViolationBridge Avoids relying on a public mutable field and eliminates any JPMS access concerns when the agent JAR is in the bootstrap classloader. --- .../solr/security/agent/SecurityViolationLogger.java | 12 ++++++------ .../apache/solr/security/AgentViolationBridge.java | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java index d3677ca296ed..486d355bf795 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SecurityViolationLogger.java @@ -36,12 +36,12 @@ */ public final class SecurityViolationLogger { - /** - * Bridge set by {@code AgentViolationBridge.wire()} once SLF4J/Log4j2 is available. {@code null} - * until wired — violations fall back to {@code System.err}. {@code volatile} for safe - * publication. - */ - public static volatile Consumer reporter = null; + private static volatile Consumer reporter = null; + + /** Sets the violation reporter. Called by {@code AgentViolationBridge.wire()} via reflection. */ + public static void setReporter(Consumer r) { + reporter = r; + } /** The operation types that can trigger a violation. */ public enum ViolationType { diff --git a/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java b/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java index 766c1195f052..c6b1187b93f5 100644 --- a/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java +++ b/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java @@ -17,7 +17,7 @@ package org.apache.solr.security; import java.lang.invoke.MethodHandles; -import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,9 +42,9 @@ public static void wire() { try { // SecurityViolationLogger is in the bootstrap classloader via Boot-Class-Path. Class cls = Class.forName("org.apache.solr.security.agent.SecurityViolationLogger"); - Field f = cls.getField("reporter"); + Method setter = cls.getMethod("setReporter", Consumer.class); Consumer bridge = msg -> log.warn("SECURITY VIOLATION {}", msg); - f.set(null, bridge); + setter.invoke(null, bridge); log.info("Security agent violation reporter wired to SLF4J"); } catch (ClassNotFoundException e) { // Agent JAR not loaded — security agent is inactive, nothing to wire From edb5ece9075fff811f049c67e7aaf1b8a4639f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 11:12:11 +0200 Subject: [PATCH 30/65] Add normalize() to FileInterceptor to prevent path-traversal bypass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without normalize(), a caller could pass paths like /var/solr/../etc/passwd — the string starts with /var/solr/ so the policy check passes, but the JDK actually opens /etc/passwd. Path.normalize() is a pure string operation (no I/O) so it is safe inside the @Advice.OnMethodEnter interceptor. Applied to all four path-resolution sites: source path and copy-target path, for both String and Path argument forms. --- .../org/apache/solr/security/agent/FileInterceptor.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java index f0149d36ade9..6cada5e639d7 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java @@ -54,9 +54,9 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin FileSystemProvider provider = null; String filePath = null; if (args.length > 0 && args[0] instanceof String pathStr) { - filePath = Path.of(pathStr).toAbsolutePath().toString(); + filePath = Path.of(pathStr).toAbsolutePath().normalize().toString(); } else if (args.length > 0 && args[0] instanceof Path path) { - filePath = path.toAbsolutePath().toString(); + filePath = path.toAbsolutePath().normalize().toString(); provider = path.getFileSystem().provider(); } @@ -125,9 +125,9 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin } } else if (name.equals("copy") == true) { if (args.length > 1 && args[1] instanceof String pathStr) { - targetFilePath = Path.of(pathStr).toAbsolutePath().toString(); + targetFilePath = Path.of(pathStr).toAbsolutePath().normalize().toString(); } else if (args.length > 1 && args[1] instanceof Path path) { - targetFilePath = path.toAbsolutePath().toString(); + targetFilePath = path.toAbsolutePath().normalize().toString(); } } } From 980d2c7eca32dc630cf3d54ae94a678594b41843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 11:13:03 +0200 Subject: [PATCH 31/65] Remove dead 'read' interceptor branch: no java.nio.file.Files.read() method There is no Files.read() method in the JDK; the high-level read APIs (readAllBytes, readString, etc.) all funnel through FileSystemProvider.newByteChannel/newInputStream which are already intercepted via 'open'/'newByteChannel'. Remove the unreachable name.equals("read") branch from FileInterceptor and drop "read" from FILE_INTERCEPTED_METHODS in SolrAgentEntryPoint. --- .../apache/solr/security/agent/FileInterceptor.java | 13 ------------- .../solr/security/agent/SolrAgentEntryPoint.java | 1 - 2 files changed, 14 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java index 6cada5e639d7..94f12e16dc99 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java @@ -180,19 +180,6 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin } } - // Plain read operations (e.g. Files.read(), Files.readAllBytes()) - if (name.equals("read") && !policy.isPathPermitted(filePath, "read")) { - ViolationMetricsReporter.incrementFile(); - SecurityViolationLogger.log( - SecurityViolationLogger.ViolationType.FILE_READ, - filePath, - caller, - policy.enforcementMode()); - if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { - throw new SecurityException("Denied READ access to file: " + filePath); - } - } - // File mutating operations if (isMutating && !policy.isPathPermitted(filePath, "write")) { ViolationMetricsReporter.incrementFile(); diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java index 67069a4da0b6..b96f1aa69a37 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java @@ -131,7 +131,6 @@ private static void bootAgent(Instrumentation inst) { "newByteChannel", "delete", "deleteIfExists", - "read", "open" }; From 971a05465e2d093c03825942185fbf6625c8571f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 11:13:56 +0200 Subject: [PATCH 32/65] Use Path.startsWith(Path) in PermittedPath to fix Windows separator handling The previous string-based startsWith check used the platform separator to build a prefix, but policy paths from variable expansion on Windows may contain forward slashes while Path.toAbsolutePath() returns backslashes, causing the check to fail. Path.startsWith(Path) delegates to the platform Path implementation which normalises separators, and also correctly respects component boundaries (preventing partial-component prefix matches). --- .../org/apache/solr/security/agent/PermittedPath.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java index 0173b320cebd..2df0ad3181ca 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java @@ -16,7 +16,7 @@ */ package org.apache.solr.security.agent; -import java.nio.file.FileSystems; +import java.nio.file.Path; import java.util.Locale; /** @@ -64,11 +64,10 @@ public PolicySource source() { public boolean permits(String resolvedPath, String action) { boolean pathMatch; if (recursive) { - // Use the platform separator so the check is correct on both POSIX and Windows. - // Strip any trailing separator from 'path' to avoid double-separator issues. - String sep = FileSystems.getDefault().getSeparator(); - String base = path.endsWith(sep) ? path : path + sep; - pathMatch = resolvedPath.startsWith(base) || resolvedPath.equals(path); + // Use Path.startsWith(Path) rather than a string prefix check so that component boundaries + // are respected and path-separator style differences (e.g. forward vs back slash on Windows) + // are handled by the platform Path implementation. + pathMatch = Path.of(resolvedPath).startsWith(Path.of(path)); } else { pathMatch = resolvedPath.equals(path); } From 58a5c59653556d4c6052c432de7e37ac3c776dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 11:14:38 +0200 Subject: [PATCH 33/65] Preserve line number and cause in PolicyFileParser.expand() exception Previously, ts.line() was called inside the catch block, and if that call itself threw IOException the original ExpandException context (the unresolved variable name) was lost in a no-arg ParsingException. Fix: capture the line number before the expand call so the IOException can be handled independently; chain the original ExpandException as the cause so stack traces retain the unresolved-variable detail. --- .../solr/security/agent/PolicyFileParser.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java index fd8c0a450a26..f9df0141922c 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java @@ -118,14 +118,22 @@ private static PermEntry parsePermEntry(PolicyTokenStream ts) } private static String expand(PolicyTokenStream ts, String raw) throws ParsingException { + // Capture the line number before attempting expansion so we preserve it if expansion fails. + int lineNum = -1; + try { + lineNum = ts.line(); + } catch (IOException ignored) { + // best-effort — -1 signals "line unknown" + } try { return PolicyPropertyExpander.expand(raw); } catch (PolicyPropertyExpander.ExpandException e) { - try { - throw new ParsingException(ts.line(), e.getMessage(), raw); - } catch (IOException ioe) { - throw new ParsingException(e.getMessage()); - } + ParsingException pe = + lineNum >= 0 + ? new ParsingException(lineNum, e.getMessage(), raw) + : new ParsingException(e.getMessage()); + pe.initCause(e); + throw pe; } } From 87fdd9889fe789892f8115de82e512a73a1b8b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 11:26:00 +0200 Subject: [PATCH 34/65] Fix test to actually verify that malformed extra policy throws ISE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous test used content ('THIS IS NOT A VALID POLICY') that has no grant blocks but is not syntactically invalid, so the parser silently produces an empty result — this contradicts the class Javadoc which states 'A malformed extra policy causes IllegalStateException'. Change the test content to a genuinely syntactically invalid policy (unclosed grant block) which causes PolicyFileParser to throw ParsingException, wrapped by PolicyLoader as IllegalStateException. Rename the test method to reflect the fail-fast contract. --- .../PolicyLoaderOperatorExtensionTest.java | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java index a6ac5864a40e..19eacc9eb201 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java @@ -161,26 +161,24 @@ public void testExtraPolicyAbsentIsNonFatal() throws Exception { } // --------------------------------------------------------------------------- - // Malformed extra policy — lenient parsing, default still enforced + // Malformed extra policy — syntactically invalid content causes ISE (fail-fast) + // (Content that parses but contains no grant blocks is silently harmless.) // --------------------------------------------------------------------------- - @Test - public void testMalformedExtraPolicyIsSkippedGracefully() throws Exception { + @Test(expected = IllegalStateException.class) + public void testMalformedExtraPolicyThrowsIllegalStateException() throws Exception { Path tmpDir = createTempDir(); Path defaultPolicy = writeDefaultPolicy(tmpDir); Path extraPolicy = tmpDir.resolve("agent-security-extra.policy"); - // Content with no recognizable grant blocks — parser silently produces empty result - Files.writeString(extraPolicy, "THIS IS NOT A VALID POLICY\n", StandardCharsets.UTF_8); + // Syntactically invalid: grant block is not closed — parser throws ParsingException which + // PolicyLoader wraps as IllegalStateException (fail-fast on malformed policy) + Files.writeString( + extraPolicy, "grant { permission java.io.FilePermission\n", StandardCharsets.UTF_8); System.setProperty("solr.security.agent.extra.policy", extraPolicy.toString()); - // Should not throw; default policy still loads - AgentPolicy policy = new PolicyLoader().load(defaultPolicy); - // Default policy (/opt/solr) is still active - assertTrue(policy.isPathPermitted("/opt/solr/conf", "read")); - // Malformed extra policy adds no new paths - assertFalse(policy.isPathPermitted(tmpDir.toString(), "read")); + new PolicyLoader().load(defaultPolicy); } // --------------------------------------------------------------------------- From 1d28b58f76a035c489644a423aaf8a5ec102bb73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 11:26:41 +0200 Subject: [PATCH 35/65] Throw ISE instead of silently logging to stderr when observableLongCounter not found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If SolrMetricManager.observableLongCounter cannot be located by the reflective simple-name lookup, the previous code printed to stderr and returned silently — metrics were then never registered without any visible warning in solr.log. Move the null check outside the try block so the thrown IllegalStateException propagates through CoreContainer's InvocationTargetException, which is caught and logged at WARN level, making the signature drift immediately visible in the Solr log. --- .../agent/ViolationMetricsReporter.java | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java index a36c9806f82b..a79e9539b7c7 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java @@ -140,26 +140,24 @@ public static long execCount() { * @param registryName the target metrics registry name (e.g. {@code "solr.node"}) */ public static void registerWithSolrMetrics(Object metricManager, String registryName) { + Class mmClass = metricManager.getClass(); + // SolrMetricManager.observableLongCounter(String registry, String name, String description, + // Consumer callback, OtelUnit unit) + // SolrMetricManager.observableLongCounter(String, String, String, Consumer, OtelUnit) + Method counterMethod = + findMethod( + mmClass, "observableLongCounter", "String", "String", "String", "Consumer", "OtelUnit"); + if (counterMethod == null) { + // Throw so that CoreContainer's reflective call site logs this at WARN. A missing method + // almost certainly means the SolrMetricManager API changed without updating this class. + throw new IllegalStateException( + "SolrMetricManager.observableLongCounter(String,String,String,Consumer,OtelUnit)" + + " not found on " + + mmClass.getName() + + " — violation metrics cannot be registered in /admin/metrics." + + " This likely indicates a SolrMetricManager API change."); + } try { - Class mmClass = metricManager.getClass(); - // SolrMetricManager.observableLongCounter(String registry, String name, String description, - // Consumer callback, OtelUnit unit) - // SolrMetricManager.observableLongCounter(String, String, String, Consumer, OtelUnit) - Method counterMethod = - findMethod( - mmClass, - "observableLongCounter", - "String", - "String", - "String", - "Consumer", - "OtelUnit"); - if (counterMethod == null) { - agentErr( - "[Solr SecurityAgent] SolrMetricManager.observableLongCounter not found" - + " — violation metrics will not be registered in /admin/metrics"); - return; - } registerCounter( counterMethod, metricManager, From 9998924b20d6479a2885fe602c822dff16fc78e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 11:27:42 +0200 Subject: [PATCH 36/65] Use null (bootstrap) classloader consistently for agent class lookups Both CoreContainer and AgentViolationBridge look up agent classes that are loaded into the bootstrap classloader via Boot-Class-Path. CoreContainer was passing CoreContainer.class.getClassLoader() (the app classloader) while AgentViolationBridge used the no-arg Class.forName() default. Use Class.forName(name, false, null) in both places so the lookup explicitly targets the bootstrap classloader, matching where the agent JAR is placed. --- solr/core/src/java/org/apache/solr/core/CoreContainer.java | 7 +++---- .../org/apache/solr/security/AgentViolationBridge.java | 5 ++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java index 418f4bfdfb86..64a30cb846d3 100644 --- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java +++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java @@ -956,11 +956,10 @@ private void loadInternal() { // Uses reflection to avoid a compile-time dependency on solr:agent-sm (see research.md Decision // 8). try { + // Use null (bootstrap classloader) to match where the agent JAR is loaded via + // Boot-Class-Path, consistent with AgentViolationBridge.wire(). Class reporter = - Class.forName( - "org.apache.solr.security.agent.ViolationMetricsReporter", - false, - CoreContainer.class.getClassLoader()); + Class.forName("org.apache.solr.security.agent.ViolationMetricsReporter", false, null); reporter .getMethod("registerWithSolrMetrics", Object.class, String.class) .invoke(null, metricManager, NODE_REGISTRY); diff --git a/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java b/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java index c6b1187b93f5..68fc4cfd1584 100644 --- a/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java +++ b/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java @@ -41,7 +41,10 @@ private AgentViolationBridge() {} public static void wire() { try { // SecurityViolationLogger is in the bootstrap classloader via Boot-Class-Path. - Class cls = Class.forName("org.apache.solr.security.agent.SecurityViolationLogger"); + // Use null (bootstrap) explicitly so the lookup succeeds even in containerised environments + // where the calling classloader may not delegate to bootstrap for unknown packages. + Class cls = + Class.forName("org.apache.solr.security.agent.SecurityViolationLogger", false, null); Method setter = cls.getMethod("setReporter", Consumer.class); Consumer bridge = msg -> log.warn("SECURITY VIOLATION {}", msg); setter.invoke(null, bridge); From 838903daf0cbd8cb9033484b858dbe9dce12cbcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 11:28:17 +0200 Subject: [PATCH 37/65] Catch Exception (not just ISE) in bootAgent to prevent NPE on unexpected errors If loader.load() threw any RuntimeException other than IllegalStateException (e.g. a NullPointerException from a parse bug), control would fall through to AgentPolicy.initialize(null), producing an unhelpful NPE during premain. Broadening the catch to Exception gives the same enforce/warn branching for all policy-load failures and avoids the null-initialize crash. --- .../org/apache/solr/security/agent/SolrAgentEntryPoint.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java index b96f1aa69a37..58ca26de86a6 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java @@ -86,7 +86,7 @@ private static void bootAgent(Instrumentation inst) { try { PolicyLoader loader = new PolicyLoader(); policy = loader.load(defaultPolicyPath); - } catch (IllegalStateException e) { + } catch (Exception e) { String modeStr = PolicyPropertyExpander.getPropertyOrEnv("solr.security.agent.mode"); if (modeStr == null) modeStr = "warn"; if ("enforce".equalsIgnoreCase(modeStr.trim())) { From c4b95694af5701e3c7135cecc7071f25b45e503e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 11:33:48 +0200 Subject: [PATCH 38/65] Fail-fast on garbage policy content; allow empty/comment-only operator files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PolicyFileParser.read() now defaults to strict mode: any top-level token that is not 'grant' throws ParsingException (wrapped as ISE by PolicyLoader). An empty file or a file containing only comments produces no tokens, so it returns an empty list without error — the OPERATOR file may legally be blank. PolicyLoader.load() additionally requires the DEFAULT policy to contain at least one grant block; an all-comment default policy now fails at startup. When the OPERATOR file is present but empty of grants, a message is printed to stderr so operators know the file was loaded but contributed nothing. Tests updated: two ISE cases (garbage tokens, unclosed block), two acceptance cases (empty file, comment-only file), and the class Javadoc is corrected to reflect the fail-fast contract. --- .../solr/security/agent/PolicyFileParser.java | 15 ++++- .../solr/security/agent/PolicyLoader.java | 19 +++++++ .../PolicyLoaderOperatorExtensionTest.java | 56 +++++++++++++++++-- 3 files changed, 83 insertions(+), 7 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java index f9df0141922c..da6ff17d0941 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java @@ -50,13 +50,26 @@ static class ParsingException extends Exception { } static List read(Reader policy) throws ParsingException, IOException { + return read(policy, true); + } + + /** + * @param strictTopLevel if {@code true}, any token that is not the start of a {@code grant} block + * causes a {@link ParsingException}; if {@code false}, such tokens are silently skipped + * (lenient mode, kept for internal use only) + */ + static List read(Reader policy, boolean strictTopLevel) + throws ParsingException, IOException { List entries = new ArrayList<>(); PolicyTokenStream ts = new PolicyTokenStream(policy); while (!ts.isEOF()) { if (peek(ts, "grant")) { entries.add(parseGrantEntry(ts)); + } else if (strictTopLevel) { + PolicyToken tok = ts.peek(); + throw new ParsingException(tok.line(), "grant", tok.text()); } else { - // skip unexpected top-level token + // skip unexpected top-level token (lenient mode) ts.consume(); } } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java index db34bc4bff8d..c7a347b29328 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java @@ -75,6 +75,12 @@ public AgentPolicy load(Path defaultPolicyPath) { List grants = new ArrayList<>(); parsePolicy(defaultContent, PolicySource.DEFAULT, grants); + if (grants.isEmpty()) { + throw new IllegalStateException( + "Security agent default policy contains no grant blocks: " + + defaultPolicyPath + + ". The default policy must define at least one grant."); + } // Resolve extra-policy path: system property → fallback to ${server.dir}/etc/... Path extraPolicyPath = resolveExtraPolicyPath(); @@ -86,7 +92,13 @@ public AgentPolicy load(Path defaultPolicyPath) { throw new IllegalStateException( "Failed to read operator security policy extension: " + extraPolicyPath, e); } + int beforeCount = grants.size(); parsePolicy(extraContent, PolicySource.OPERATOR, grants); + if (grants.size() == beforeCount) { + agentOut( + "[Solr SecurityAgent] Operator extension policy is empty (no grant blocks): " + + extraPolicyPath); + } } return buildPolicy(grants); @@ -287,4 +299,11 @@ static class RawRuntimePermission { this.source = source; } } + + @SuppressForbidden( + reason = + "System.err is the only safe output channel during premain, before SLF4J is available.") + private static void agentOut(String msg) { + System.err.println(msg); + } } diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java index 19eacc9eb201..74b4d20e96c1 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderOperatorExtensionTest.java @@ -35,7 +35,8 @@ *
    • Entries from the extra policy are tagged {@link PolicySource#OPERATOR}. *
    • Paths listed only in the extra policy are permitted; unlisted paths remain blocked. *
    • When the extra policy file is absent the default policy still loads normally. - *
    • A malformed extra policy causes {@link IllegalStateException} with a descriptive message. + *
    • Any non-comment content that is not a valid grant causes {@link IllegalStateException} + * (fail-fast). An empty file or a file containing only comments is silently accepted. *
    • The {@code source=OPERATOR} tag is emitted in violation log entries for paths matched by * operator-policy entries. *
    @@ -161,18 +162,30 @@ public void testExtraPolicyAbsentIsNonFatal() throws Exception { } // --------------------------------------------------------------------------- - // Malformed extra policy — syntactically invalid content causes ISE (fail-fast) - // (Content that parses but contains no grant blocks is silently harmless.) + // Malformed extra policy — any non-comment content that is not a valid grant causes ISE // --------------------------------------------------------------------------- @Test(expected = IllegalStateException.class) - public void testMalformedExtraPolicyThrowsIllegalStateException() throws Exception { + public void testGarbageExtraPolicyThrowsIllegalStateException() throws Exception { Path tmpDir = createTempDir(); Path defaultPolicy = writeDefaultPolicy(tmpDir); Path extraPolicy = tmpDir.resolve("agent-security-extra.policy"); - // Syntactically invalid: grant block is not closed — parser throws ParsingException which - // PolicyLoader wraps as IllegalStateException (fail-fast on malformed policy) + // Non-comment content that is not a valid grant block → fail fast + Files.writeString(extraPolicy, "THIS IS NOT A VALID POLICY\n", StandardCharsets.UTF_8); + + System.setProperty("solr.security.agent.extra.policy", extraPolicy.toString()); + + new PolicyLoader().load(defaultPolicy); + } + + @Test(expected = IllegalStateException.class) + public void testUnclosedGrantExtraPolicyThrowsIllegalStateException() throws Exception { + Path tmpDir = createTempDir(); + Path defaultPolicy = writeDefaultPolicy(tmpDir); + + Path extraPolicy = tmpDir.resolve("agent-security-extra.policy"); + // Syntactically invalid: unclosed grant block Files.writeString( extraPolicy, "grant { permission java.io.FilePermission\n", StandardCharsets.UTF_8); @@ -181,6 +194,37 @@ public void testMalformedExtraPolicyThrowsIllegalStateException() throws Excepti new PolicyLoader().load(defaultPolicy); } + @Test + public void testEmptyExtraPolicyIsAccepted() throws Exception { + Path tmpDir = createTempDir(); + Path defaultPolicy = writeDefaultPolicy(tmpDir); + + Path extraPolicy = tmpDir.resolve("agent-security-extra.policy"); + Files.writeString(extraPolicy, "", StandardCharsets.UTF_8); + + System.setProperty("solr.security.agent.extra.policy", extraPolicy.toString()); + + // Empty operator file is silently accepted; default policy is still active + AgentPolicy policy = new PolicyLoader().load(defaultPolicy); + assertTrue(policy.isPathPermitted("/opt/solr/conf", "read")); + } + + @Test + public void testCommentOnlyExtraPolicyIsAccepted() throws Exception { + Path tmpDir = createTempDir(); + Path defaultPolicy = writeDefaultPolicy(tmpDir); + + Path extraPolicy = tmpDir.resolve("agent-security-extra.policy"); + Files.writeString( + extraPolicy, "// This is a comment\n/* Block comment too */\n", StandardCharsets.UTF_8); + + System.setProperty("solr.security.agent.extra.policy", extraPolicy.toString()); + + // Comment-only operator file is silently accepted; default policy is still active + AgentPolicy policy = new PolicyLoader().load(defaultPolicy); + assertTrue(policy.isPathPermitted("/opt/solr/conf", "read")); + } + // --------------------------------------------------------------------------- // Source field in violation log // --------------------------------------------------------------------------- From ec0f317dc0f49b7a0d50fb5a6e0ac2426c4395f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 11:45:52 +0200 Subject: [PATCH 39/65] Use single labeled OTel counter for violation metrics instead of four separate counters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OTel best practice is one metric name with a dimension label rather than separate metrics per dimension value. Before: four counters solr_security_agent_violations_file_total solr_security_agent_violations_network_total solr_security_agent_violations_exit_total solr_security_agent_violations_exec_total After: one counter with type label solr_security_agent_violations_total{type="file"} solr_security_agent_violations_total{type="network"} solr_security_agent_violations_total{type="exit"} solr_security_agent_violations_total{type="exec"} The existing SolrMetricManager.observableLongCounter API is sufficient — the same pattern (measurement.record(value, Attributes)) is already used by OtelRuntimeJvmMetrics for JVM memory metrics. Because ViolationMetricsReporter lives in the bootstrap classloader (no OTel compile dependency), Attributes instances are created reflectively via the app classloader at registration time and captured in the callback closure. --- .../agent/ViolationMetricsReporter.java | 149 +++++++++--------- solr/packaging/test/test_security_agent.bats | 8 +- .../pages/security-agent.adoc | 17 +- 3 files changed, 93 insertions(+), 81 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java index a79e9539b7c7..9b643e80d7b8 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java @@ -17,9 +17,9 @@ package org.apache.solr.security.agent; import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.LongAdder; import java.util.function.Consumer; -import java.util.function.Supplier; /** * Maintains per-type violation counters and registers them with Solr's metrics registry once it @@ -34,16 +34,18 @@ * {@code solr:agent-sm} from {@code solr:core}). At that point the accumulated counts are already * in the counters and the registration just wires them to the metrics registry. * - *

    Metric names

    + *

    Metric name and label

    * - *
      - *
    • {@code solr.security.agent.violations.file} - *
    • {@code solr.security.agent.violations.network} - *
    • {@code solr.security.agent.violations.exit} - *
    • {@code solr.security.agent.violations.exec} - *
    + * A single OTel observable counter {@value #METRIC_NAME} is registered with the label key {@value + * #LABEL_TYPE}. The label takes one of four values: {@code file}, {@code network}, {@code exit}, or + * {@code exec}. In Prometheus format the counter appears as: * - *

    In Prometheus format these appear as {@code solr_security_agent_violations_file_total}, etc. + *

    + *   solr_security_agent_violations_total{type="file"}    N
    + *   solr_security_agent_violations_total{type="network"} N
    + *   solr_security_agent_violations_total{type="exit"}    N
    + *   solr_security_agent_violations_total{type="exec"}    N
    + * 
    */ public final class ViolationMetricsReporter { @@ -53,11 +55,11 @@ public final class ViolationMetricsReporter { private static final LongAdder EXIT_COUNTER = new LongAdder(); private static final LongAdder EXEC_COUNTER = new LongAdder(); - // Metric names exposed in the Solr metrics registry. - public static final String METRIC_FILE = "solr.security.agent.violations.file"; - public static final String METRIC_NETWORK = "solr.security.agent.violations.network"; - public static final String METRIC_EXIT = "solr.security.agent.violations.exit"; - public static final String METRIC_EXEC = "solr.security.agent.violations.exec"; + /** OTel metric name for the single labeled violation counter. */ + public static final String METRIC_NAME = "solr.security.agent.violations"; + + /** OTel label key used to distinguish violation types. */ + public static final String LABEL_TYPE = "type"; private ViolationMetricsReporter() {} @@ -114,8 +116,7 @@ public static long execCount() { // --------------------------------------------------------------------------- /** - * Registers the four violation counters with the given {@code SolrMetricManager} in the specified - * registry. + * Registers a single labeled violation counter with the given {@code SolrMetricManager}. * *

    This method is called reflectively from {@code CoreContainer} to avoid a compile-time * dependency between {@code solr:core} and {@code solr:agent-sm}. The signature must match what @@ -131,19 +132,19 @@ public static long execCount() { * is declared as {@link Object}; the reflective call site in {@code CoreContainer} passes the * real {@code SolrMetricManager} instance. * - *

    Metrics are registered as OTel observable counters via {@code - * SolrMetricManager.observableLongCounter()}. In Prometheus format they appear as {@code - * solr_security_agent_violations_file_total} etc. + *

    One OTel observable counter named {@value #METRIC_NAME} is registered. The callback records + * four labeled values (one per violation type) using {@code + * ObservableLongMeasurement.record(long, Attributes)}. {@code io.opentelemetry.api.common + * .Attributes} instances are created reflectively via the app classloader because this class + * lives in the bootstrap classloader and has no OTel dependency. * - * @param metricManager the {@code SolrMetricManager} instance (type-erased to {@link Object} to - * avoid a compile-time dependency on solr:core) + * @param metricManager the {@code SolrMetricManager} instance (type-erased to {@link Object}) * @param registryName the target metrics registry name (e.g. {@code "solr.node"}) */ public static void registerWithSolrMetrics(Object metricManager, String registryName) { Class mmClass = metricManager.getClass(); // SolrMetricManager.observableLongCounter(String registry, String name, String description, // Consumer callback, OtelUnit unit) - // SolrMetricManager.observableLongCounter(String, String, String, Consumer, OtelUnit) Method counterMethod = findMethod( mmClass, "observableLongCounter", "String", "String", "String", "Consumer", "OtelUnit"); @@ -158,36 +159,63 @@ public static void registerWithSolrMetrics(Object metricManager, String registry + " This likely indicates a SolrMetricManager API change."); } try { - registerCounter( - counterMethod, - metricManager, - registryName, - METRIC_FILE, - "Security agent file-access violation count", - FILE_COUNTER::sum); - registerCounter( - counterMethod, - metricManager, - registryName, - METRIC_NETWORK, - "Security agent network-connection violation count", - NETWORK_COUNTER::sum); - registerCounter( - counterMethod, + // Build Attributes objects reflectively — this class is in the bootstrap classloader with no + // OTel on its classpath. Use the app classloader (reachable via metricManager) to access + // io.opentelemetry.api.common.{AttributeKey, Attributes} at registration time. + ClassLoader cl = metricManager.getClass().getClassLoader(); + Class attrKeyClass = Class.forName("io.opentelemetry.api.common.AttributeKey", false, cl); + Class attrsClass = Class.forName("io.opentelemetry.api.common.Attributes", false, cl); + // AttributeKey.stringKey("type") — produces AttributeKey + Object typeKey = attrKeyClass.getMethod("stringKey", String.class).invoke(null, LABEL_TYPE); + // Attributes.of(AttributeKey, T) — after type erasure: (AttributeKey, Object) + Method attrOf = attrsClass.getMethod("of", attrKeyClass, Object.class); + final Object fileAttrs = attrOf.invoke(null, typeKey, "file"); + final Object networkAttrs = attrOf.invoke(null, typeKey, "network"); + final Object exitAttrs = attrOf.invoke(null, typeKey, "exit"); + final Object execAttrs = attrOf.invoke(null, typeKey, "exec"); + + // Cache the two-arg record(long, Attributes) method, found lazily on first callback + // invocation. AtomicReference is used because lambda captures must be effectively final. + final AtomicReference recordRef = new AtomicReference<>(); + + Consumer callback = + measurement -> { + try { + Method record = recordRef.get(); + if (record == null) { + // Locate record(long, ) on the concrete ObservableLongMeasurement impl. + // The second parameter type is Attributes, erased to Object at the call site — + // scan by arity and first-param type to avoid a hard dependency on + // Attributes.class. + for (Method m : measurement.getClass().getMethods()) { + if ("record".equals(m.getName()) + && m.getParameterCount() == 2 + && m.getParameterTypes()[0] == long.class) { + recordRef.compareAndSet(null, m); + break; + } + } + record = recordRef.get(); + } + if (record != null) { + record.invoke(measurement, FILE_COUNTER.sum(), fileAttrs); + record.invoke(measurement, NETWORK_COUNTER.sum(), networkAttrs); + record.invoke(measurement, EXIT_COUNTER.sum(), exitAttrs); + record.invoke(measurement, EXEC_COUNTER.sum(), execAttrs); + } + } catch (ReflectiveOperationException ignored) { + // Silently skip if record(long, Attributes) is unavailable + } + }; + + counterMethod.invoke( metricManager, registryName, - METRIC_EXIT, - "Security agent JVM-exit violation count", - EXIT_COUNTER::sum); - registerCounter( - counterMethod, - metricManager, - registryName, - METRIC_EXEC, - "Security agent process-exec violation count", - EXEC_COUNTER::sum); + METRIC_NAME, + "Security agent violation count by type (file, network, exit, exec).", + callback, + null); } catch (Exception e) { - // Log to stderr — SLF4J may not be reachable from bootstrap context during premain. agentErr("[Solr SecurityAgent] Failed to register violation metrics: " + e); } } @@ -213,29 +241,6 @@ private static Method findMethod(Class cls, String name, String... paramTypeS return null; } - private static void registerCounter( - Method counterMethod, - Object mm, - String registry, - String name, - String description, - Supplier valueSupplier) - throws Exception { - // Consumer — type-erased to Consumer at runtime. - // Called by the OTel SDK at each metric collection cycle. - Consumer callback = - measurement -> { - try { - Method record = measurement.getClass().getMethod("record", long.class); - record.invoke(measurement, valueSupplier.get()); - } catch (ReflectiveOperationException ignored) { - // Silently skip if ObservableLongMeasurement.record() is unavailable - } - }; - // observableLongCounter(registry, name, description, callback, unit=null) - counterMethod.invoke(mm, registry, name, description, callback, null); - } - @SuppressForbidden( reason = "System.err is the only output channel available during premain/agent bootstrap, " diff --git a/solr/packaging/test/test_security_agent.bats b/solr/packaging/test/test_security_agent.bats index 03bbf1d11a51..b487a390bbe5 100644 --- a/solr/packaging/test/test_security_agent.bats +++ b/solr/packaging/test/test_security_agent.bats @@ -59,12 +59,12 @@ console_log() { assert_output --partial "mode=WARN" # Confirms: CoreContainer reflective call to ViolationMetricsReporter registered counters. - # Metrics appear in Prometheus format (solr.security.agent.violations.file → - # solr_security_agent_violations_file_total) since SolrMetricManager is OTel-based. + # A single labeled counter solr.security.agent.violations appears in Prometheus format as + # solr_security_agent_violations_total{type="file"|"network"|"exit"|"exec"}. run curl -sf "http://localhost:${SOLR_PORT}/solr/admin/metrics" assert_success - assert_output --partial "solr_security_agent_violations_file" - assert_output --partial "solr_security_agent_violations_network" + assert_output --partial 'solr_security_agent_violations_total{type="file"' + assert_output --partial 'solr_security_agent_violations_total{type="network"' } @test "SOLR_SECURITY_AGENT_SKIP=true disables the agent" { diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc index 3c564193cbd5..a2e9eef7c541 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc @@ -126,12 +126,19 @@ Fields: * `mode` — `warn` or `enforce` * `source` — `DEFAULT` (bundled policy) or `OPERATOR` (extra policy) -Violation counts are also available in `/admin/metrics` under: +Violation counts are also available in `/admin/metrics` as a single labeled counter: -* `solr.security.agent.violations.file` -* `solr.security.agent.violations.network` -* `solr.security.agent.violations.exit` -* `solr.security.agent.violations.exec` +* Metric name: `solr.security.agent.violations` +* Label `type`: `file`, `network`, `exit`, or `exec` + +In Prometheus format: +[source,text] +---- +solr_security_agent_violations_total{type="file"} 3.0 +solr_security_agent_violations_total{type="network"} 0.0 +solr_security_agent_violations_total{type="exit"} 0.0 +solr_security_agent_violations_total{type="exec"} 0.0 +---- == Disabling the Agent From e62e5ce9d712fb789a2d7cd310182078639ad30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 11:53:52 +0200 Subject: [PATCH 40/65] Move OTel metric registration to AgentViolationMetrics in solr:core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ViolationMetricsReporter (bootstrap classloader, no OTel) is now just LongAdder counters + increment/count methods — no reflection, no OTel, no registration logic. New AgentViolationMetrics (org.apache.solr.security, solr:core) owns all OTel registration: it uses AttributeKey/Attributes/ObservableLongMeasurement natively, builds the labeled callback directly, and registers one counter via SolrMetricManager. The only reflection is reading the four plain-long counter values from the bootstrap-loaded ViolationMetricsReporter. CoreContainer replaces the verbose try/catch reflective block with a single AgentViolationMetrics.register(metricManager, NODE_REGISTRY) call; the ClassNotFoundException / warn handling is now encapsulated in that class. --- .../agent/ViolationMetricsReporter.java | 183 ++---------------- .../org/apache/solr/core/CoreContainer.java | 17 +- .../solr/security/AgentViolationMetrics.java | 106 ++++++++++ 3 files changed, 119 insertions(+), 187 deletions(-) create mode 100644 solr/core/src/java/org/apache/solr/security/AgentViolationMetrics.java diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java index 9b643e80d7b8..2b88e768d44a 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java @@ -16,36 +16,18 @@ */ package org.apache.solr.security.agent; -import java.lang.reflect.Method; -import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.LongAdder; -import java.util.function.Consumer; /** - * Maintains per-type violation counters and registers them with Solr's metrics registry once it - * becomes available. + * Maintains per-type violation counters incremented by the agent interceptors. * - *

    Deferred registration pattern

    + *

    The agent starts (via {@code premain()}) before Solr is fully initialized, so counters must be + * available immediately. They are implemented as {@link LongAdder}s which are safe for concurrent + * increment from multiple interceptor threads. * - * The agent starts (via {@code premain()}) before Solr's {@code SolrMetricManager} is initialized. - * Counters are maintained from the very first violation using {@link LongAdder}s. When {@code - * CoreContainer} initializes {@code SolrMetricManager}, it calls {@link - * #registerWithSolrMetrics(Object, String)} reflectively (to avoid a compile-time dependency on - * {@code solr:agent-sm} from {@code solr:core}). At that point the accumulated counts are already - * in the counters and the registration just wires them to the metrics registry. - * - *

    Metric name and label

    - * - * A single OTel observable counter {@value #METRIC_NAME} is registered with the label key {@value - * #LABEL_TYPE}. The label takes one of four values: {@code file}, {@code network}, {@code exit}, or - * {@code exec}. In Prometheus format the counter appears as: - * - *
    - *   solr_security_agent_violations_total{type="file"}    N
    - *   solr_security_agent_violations_total{type="network"} N
    - *   solr_security_agent_violations_total{type="exit"}    N
    - *   solr_security_agent_violations_total{type="exec"}    N
    - * 
    + *

    Once {@code CoreContainer} and {@code SolrMetricManager} are ready, {@code + * AgentViolationMetrics} (in {@code solr:core}) reads these counters reflectively and registers + * them as a single labeled OTel observable counter. */ public final class ViolationMetricsReporter { @@ -55,12 +37,6 @@ public final class ViolationMetricsReporter { private static final LongAdder EXIT_COUNTER = new LongAdder(); private static final LongAdder EXEC_COUNTER = new LongAdder(); - /** OTel metric name for the single labeled violation counter. */ - public static final String METRIC_NAME = "solr.security.agent.violations"; - - /** OTel label key used to distinguish violation types. */ - public static final String LABEL_TYPE = "type"; - private ViolationMetricsReporter() {} // --------------------------------------------------------------------------- @@ -77,7 +53,7 @@ public static void incrementNetwork() { NETWORK_COUNTER.increment(); } - /** Increments the System.exit() violation counter. */ + /** Increments the System.exit() / Runtime.halt() violation counter. */ public static void incrementExit() { EXIT_COUNTER.increment(); } @@ -88,7 +64,8 @@ public static void incrementExec() { } // --------------------------------------------------------------------------- - // Counter read API (used by tests) + // Counter read API (called reflectively by AgentViolationMetrics in solr:core, + // and used directly by tests) // --------------------------------------------------------------------------- /** Returns the current file-access violation count. */ @@ -101,7 +78,7 @@ public static long networkCount() { return NETWORK_COUNTER.sum(); } - /** Returns the current System.exit() violation count. */ + /** Returns the current System.exit() / Runtime.halt() violation count. */ public static long exitCount() { return EXIT_COUNTER.sum(); } @@ -110,142 +87,4 @@ public static long exitCount() { public static long execCount() { return EXEC_COUNTER.sum(); } - - // --------------------------------------------------------------------------- - // Deferred metrics registration (called reflectively from CoreContainer) - // --------------------------------------------------------------------------- - - /** - * Registers a single labeled violation counter with the given {@code SolrMetricManager}. - * - *

    This method is called reflectively from {@code CoreContainer} to avoid a compile-time - * dependency between {@code solr:core} and {@code solr:agent-sm}. The signature must match what - * CoreContainer expects: - * - *

    {@code
    -   * Class.forName("org.apache.solr.security.agent.ViolationMetricsReporter", false, null)
    -   *      .getMethod("registerWithSolrMetrics", Object.class, String.class)
    -   *      .invoke(null, metricManager, "solr.node");
    -   * }
    - * - *

    Because this module has no compile-time dependency on {@code solr:core}, the parameter type - * is declared as {@link Object}; the reflective call site in {@code CoreContainer} passes the - * real {@code SolrMetricManager} instance. - * - *

    One OTel observable counter named {@value #METRIC_NAME} is registered. The callback records - * four labeled values (one per violation type) using {@code - * ObservableLongMeasurement.record(long, Attributes)}. {@code io.opentelemetry.api.common - * .Attributes} instances are created reflectively via the app classloader because this class - * lives in the bootstrap classloader and has no OTel dependency. - * - * @param metricManager the {@code SolrMetricManager} instance (type-erased to {@link Object}) - * @param registryName the target metrics registry name (e.g. {@code "solr.node"}) - */ - public static void registerWithSolrMetrics(Object metricManager, String registryName) { - Class mmClass = metricManager.getClass(); - // SolrMetricManager.observableLongCounter(String registry, String name, String description, - // Consumer callback, OtelUnit unit) - Method counterMethod = - findMethod( - mmClass, "observableLongCounter", "String", "String", "String", "Consumer", "OtelUnit"); - if (counterMethod == null) { - // Throw so that CoreContainer's reflective call site logs this at WARN. A missing method - // almost certainly means the SolrMetricManager API changed without updating this class. - throw new IllegalStateException( - "SolrMetricManager.observableLongCounter(String,String,String,Consumer,OtelUnit)" - + " not found on " - + mmClass.getName() - + " — violation metrics cannot be registered in /admin/metrics." - + " This likely indicates a SolrMetricManager API change."); - } - try { - // Build Attributes objects reflectively — this class is in the bootstrap classloader with no - // OTel on its classpath. Use the app classloader (reachable via metricManager) to access - // io.opentelemetry.api.common.{AttributeKey, Attributes} at registration time. - ClassLoader cl = metricManager.getClass().getClassLoader(); - Class attrKeyClass = Class.forName("io.opentelemetry.api.common.AttributeKey", false, cl); - Class attrsClass = Class.forName("io.opentelemetry.api.common.Attributes", false, cl); - // AttributeKey.stringKey("type") — produces AttributeKey - Object typeKey = attrKeyClass.getMethod("stringKey", String.class).invoke(null, LABEL_TYPE); - // Attributes.of(AttributeKey, T) — after type erasure: (AttributeKey, Object) - Method attrOf = attrsClass.getMethod("of", attrKeyClass, Object.class); - final Object fileAttrs = attrOf.invoke(null, typeKey, "file"); - final Object networkAttrs = attrOf.invoke(null, typeKey, "network"); - final Object exitAttrs = attrOf.invoke(null, typeKey, "exit"); - final Object execAttrs = attrOf.invoke(null, typeKey, "exec"); - - // Cache the two-arg record(long, Attributes) method, found lazily on first callback - // invocation. AtomicReference is used because lambda captures must be effectively final. - final AtomicReference recordRef = new AtomicReference<>(); - - Consumer callback = - measurement -> { - try { - Method record = recordRef.get(); - if (record == null) { - // Locate record(long, ) on the concrete ObservableLongMeasurement impl. - // The second parameter type is Attributes, erased to Object at the call site — - // scan by arity and first-param type to avoid a hard dependency on - // Attributes.class. - for (Method m : measurement.getClass().getMethods()) { - if ("record".equals(m.getName()) - && m.getParameterCount() == 2 - && m.getParameterTypes()[0] == long.class) { - recordRef.compareAndSet(null, m); - break; - } - } - record = recordRef.get(); - } - if (record != null) { - record.invoke(measurement, FILE_COUNTER.sum(), fileAttrs); - record.invoke(measurement, NETWORK_COUNTER.sum(), networkAttrs); - record.invoke(measurement, EXIT_COUNTER.sum(), exitAttrs); - record.invoke(measurement, EXEC_COUNTER.sum(), execAttrs); - } - } catch (ReflectiveOperationException ignored) { - // Silently skip if record(long, Attributes) is unavailable - } - }; - - counterMethod.invoke( - metricManager, - registryName, - METRIC_NAME, - "Security agent violation count by type (file, network, exit, exec).", - callback, - null); - } catch (Exception e) { - agentErr("[Solr SecurityAgent] Failed to register violation metrics: " + e); - } - } - - /** - * Finds a public method on {@code cls} by name and parameter type simple-names. Using simple - * names avoids a compile-time dependency on {@code solr:core} types (e.g. {@code OtelUnit}). - */ - private static Method findMethod(Class cls, String name, String... paramTypeSimpleNames) { - for (Method m : cls.getMethods()) { - if (!name.equals(m.getName())) continue; - Class[] params = m.getParameterTypes(); - if (params.length != paramTypeSimpleNames.length) continue; - boolean match = true; - for (int i = 0; i < params.length; i++) { - if (!params[i].getSimpleName().equals(paramTypeSimpleNames[i])) { - match = false; - break; - } - } - if (match) return m; - } - return null; - } - - @SuppressForbidden( - reason = - "System.err is the only output channel available during premain/agent bootstrap, " - + "before SLF4J is reachable from the bootstrap classloader.") - private static void agentErr(String msg) { - System.err.println(msg); - } } diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java index 64a30cb846d3..08b97abbfa60 100644 --- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java +++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java @@ -146,6 +146,7 @@ import org.apache.solr.search.SolrFieldCacheBean; import org.apache.solr.search.SolrIndexSearcher; import org.apache.solr.security.AgentViolationBridge; +import org.apache.solr.security.AgentViolationMetrics; import org.apache.solr.security.AllowListUrlChecker; import org.apache.solr.security.AuditEvent; import org.apache.solr.security.AuditLoggerPlugin; @@ -953,21 +954,7 @@ private void loadInternal() { solrMetricsContext, Attributes.of(CATEGORY_ATTR, SolrInfoBean.Category.CACHE.toString())); // Register security agent violation metrics if the agent is loaded. - // Uses reflection to avoid a compile-time dependency on solr:agent-sm (see research.md Decision - // 8). - try { - // Use null (bootstrap classloader) to match where the agent JAR is loaded via - // Boot-Class-Path, consistent with AgentViolationBridge.wire(). - Class reporter = - Class.forName("org.apache.solr.security.agent.ViolationMetricsReporter", false, null); - reporter - .getMethod("registerWithSolrMetrics", Object.class, String.class) - .invoke(null, metricManager, NODE_REGISTRY); - } catch (ClassNotFoundException ignored) { - // Agent not loaded (e.g. SOLR_SECURITY_AGENT_SKIP=true); metrics registration skipped. - } catch (ReflectiveOperationException e) { - log.warn("Failed to register security agent metrics", e); - } + AgentViolationMetrics.register(metricManager, NODE_REGISTRY); // Wire security agent violations to SLF4J; no-op if agent JAR is absent. AgentViolationBridge.wire(); diff --git a/solr/core/src/java/org/apache/solr/security/AgentViolationMetrics.java b/solr/core/src/java/org/apache/solr/security/AgentViolationMetrics.java new file mode 100644 index 000000000000..9f132ca104f2 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/security/AgentViolationMetrics.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import java.util.function.Consumer; +import org.apache.solr.metrics.SolrMetricManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Registers the security-agent violation counter with {@link SolrMetricManager} using a proper OTel + * label, keeping all {@code io.opentelemetry} types inside {@code solr:core} where they belong. + * + *

    The agent JAR ({@code solr:agent-sm}) lives in the bootstrap classloader and has no OTel + * compile dependency. This class bridges the gap: it reads the raw {@code long} counts from {@code + * ViolationMetricsReporter} reflectively (trivial — only {@code long} primitives cross the + * boundary), and builds the full {@link Consumer}{@code <}{@link ObservableLongMeasurement}{@code + * >} callback using OTel types natively. + * + *

    A single OTel observable counter named {@code solr.security.agent.violations} is registered, + * with label {@code type=file|network|exit|exec}. In Prometheus format: + * + *

    + *   solr_security_agent_violations_total{type="file"}    N
    + *   solr_security_agent_violations_total{type="network"} N
    + *   solr_security_agent_violations_total{type="exit"}    N
    + *   solr_security_agent_violations_total{type="exec"}    N
    + * 
    + */ +public final class AgentViolationMetrics { + + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + static final String METRIC_NAME = "solr.security.agent.violations"; + private static final AttributeKey TYPE_KEY = AttributeKey.stringKey("type"); + + private AgentViolationMetrics() {} + + /** + * Registers the violation counter. No-op if the agent JAR is not present. + * + * @param metricManager the live {@link SolrMetricManager} + * @param registryName the target registry (e.g. {@code "solr.node"}) + */ + public static void register(SolrMetricManager metricManager, String registryName) { + try { + // ViolationMetricsReporter is in the bootstrap classloader via Boot-Class-Path. + Class reporter = + Class.forName("org.apache.solr.security.agent.ViolationMetricsReporter", false, null); + Method fileCount = reporter.getMethod("fileCount"); + Method networkCount = reporter.getMethod("networkCount"); + Method exitCount = reporter.getMethod("exitCount"); + Method execCount = reporter.getMethod("execCount"); + + // Pre-build Attributes once — these are stable for the lifetime of the process. + Attributes fileAttrs = Attributes.of(TYPE_KEY, "file"); + Attributes networkAttrs = Attributes.of(TYPE_KEY, "network"); + Attributes exitAttrs = Attributes.of(TYPE_KEY, "exit"); + Attributes execAttrs = Attributes.of(TYPE_KEY, "exec"); + + Consumer callback = + measurement -> { + try { + measurement.record((long) fileCount.invoke(null), fileAttrs); + measurement.record((long) networkCount.invoke(null), networkAttrs); + measurement.record((long) exitCount.invoke(null), exitAttrs); + measurement.record((long) execCount.invoke(null), execAttrs); + } catch (ReflectiveOperationException ignored) { + // Should never happen — these are simple no-arg static methods. + } + }; + + metricManager.observableLongCounter( + registryName, + METRIC_NAME, + "Security agent violation count by type (file, network, exit, exec).", + callback, + null); + + log.debug("Security agent violation metrics registered under registry '{}'", registryName); + } catch (ClassNotFoundException ignored) { + // Agent JAR not loaded (e.g. SOLR_SECURITY_AGENT_SKIP=true) — nothing to register. + } catch (Exception e) { + log.warn("Failed to register security agent violation metrics", e); + } + } +} From cf90b0ca32913148e22e0f49b5155ce9d61414f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 12:05:59 +0200 Subject: [PATCH 41/65] Simplify javadoc and inline comments in agent-sm classes Remove verbose explanations, over-specified cross-references, and implementation-detail comments that are obvious from the code. --- .../solr/security/agent/PermittedPath.java | 4 +--- .../solr/security/agent/PolicyFileParser.java | 9 +++------ .../agent/ViolationMetricsReporter.java | 20 +++---------------- .../solr/security/AgentViolationBridge.java | 7 +++---- .../solr/security/AgentViolationMetrics.java | 18 ++++++----------- 5 files changed, 16 insertions(+), 42 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java index 2df0ad3181ca..3803b4b059f7 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java @@ -64,9 +64,7 @@ public PolicySource source() { public boolean permits(String resolvedPath, String action) { boolean pathMatch; if (recursive) { - // Use Path.startsWith(Path) rather than a string prefix check so that component boundaries - // are respected and path-separator style differences (e.g. forward vs back slash on Windows) - // are handled by the platform Path implementation. + // Path.startsWith(Path) respects component boundaries and cross-platform separator style. pathMatch = Path.of(resolvedPath).startsWith(Path.of(path)); } else { pathMatch = resolvedPath.equals(path); diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java index da6ff17d0941..ccb55def8d93 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyFileParser.java @@ -54,9 +54,8 @@ static List read(Reader policy) throws ParsingException, IOException } /** - * @param strictTopLevel if {@code true}, any token that is not the start of a {@code grant} block - * causes a {@link ParsingException}; if {@code false}, such tokens are silently skipped - * (lenient mode, kept for internal use only) + * @param strictTopLevel if {@code true}, non-{@code grant} top-level tokens throw {@link + * ParsingException}; if {@code false}, they are silently skipped */ static List read(Reader policy, boolean strictTopLevel) throws ParsingException, IOException { @@ -69,7 +68,6 @@ static List read(Reader policy, boolean strictTopLevel) PolicyToken tok = ts.peek(); throw new ParsingException(tok.line(), "grant", tok.text()); } else { - // skip unexpected top-level token (lenient mode) ts.consume(); } } @@ -131,12 +129,11 @@ private static PermEntry parsePermEntry(PolicyTokenStream ts) } private static String expand(PolicyTokenStream ts, String raw) throws ParsingException { - // Capture the line number before attempting expansion so we preserve it if expansion fails. int lineNum = -1; try { lineNum = ts.line(); } catch (IOException ignored) { - // best-effort — -1 signals "line unknown" + // -1 = line unknown } try { return PolicyPropertyExpander.expand(raw); diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java index 2b88e768d44a..5341702ca097 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java @@ -19,15 +19,10 @@ import java.util.concurrent.atomic.LongAdder; /** - * Maintains per-type violation counters incremented by the agent interceptors. + * Per-type violation counters incremented by the agent interceptors. * - *

    The agent starts (via {@code premain()}) before Solr is fully initialized, so counters must be - * available immediately. They are implemented as {@link LongAdder}s which are safe for concurrent - * increment from multiple interceptor threads. - * - *

    Once {@code CoreContainer} and {@code SolrMetricManager} are ready, {@code - * AgentViolationMetrics} (in {@code solr:core}) reads these counters reflectively and registers - * them as a single labeled OTel observable counter. + *

    {@code AgentViolationMetrics} (in {@code solr:core}) reads these counters reflectively and + * registers them as a single labeled OTel observable counter. */ public final class ViolationMetricsReporter { @@ -39,10 +34,6 @@ public final class ViolationMetricsReporter { private ViolationMetricsReporter() {} - // --------------------------------------------------------------------------- - // Counter increment API (called by interceptors) - // --------------------------------------------------------------------------- - /** Increments the file-access violation counter. */ public static void incrementFile() { FILE_COUNTER.increment(); @@ -63,11 +54,6 @@ public static void incrementExec() { EXEC_COUNTER.increment(); } - // --------------------------------------------------------------------------- - // Counter read API (called reflectively by AgentViolationMetrics in solr:core, - // and used directly by tests) - // --------------------------------------------------------------------------- - /** Returns the current file-access violation count. */ public static long fileCount() { return FILE_COUNTER.sum(); diff --git a/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java b/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java index 68fc4cfd1584..35434be28592 100644 --- a/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java +++ b/solr/core/src/java/org/apache/solr/security/AgentViolationBridge.java @@ -40,9 +40,8 @@ private AgentViolationBridge() {} */ public static void wire() { try { - // SecurityViolationLogger is in the bootstrap classloader via Boot-Class-Path. - // Use null (bootstrap) explicitly so the lookup succeeds even in containerised environments - // where the calling classloader may not delegate to bootstrap for unknown packages. + // null = bootstrap classloader (Boot-Class-Path); avoids delegation gaps in containerised + // envs. Class cls = Class.forName("org.apache.solr.security.agent.SecurityViolationLogger", false, null); Method setter = cls.getMethod("setReporter", Consumer.class); @@ -50,7 +49,7 @@ public static void wire() { setter.invoke(null, bridge); log.info("Security agent violation reporter wired to SLF4J"); } catch (ClassNotFoundException e) { - // Agent JAR not loaded — security agent is inactive, nothing to wire + // Agent JAR not loaded — nothing to wire. } catch (Exception e) { log.warn("Could not wire security agent violation reporter to SLF4J", e); } diff --git a/solr/core/src/java/org/apache/solr/security/AgentViolationMetrics.java b/solr/core/src/java/org/apache/solr/security/AgentViolationMetrics.java index 9f132ca104f2..864ee7caeec7 100644 --- a/solr/core/src/java/org/apache/solr/security/AgentViolationMetrics.java +++ b/solr/core/src/java/org/apache/solr/security/AgentViolationMetrics.java @@ -27,17 +27,12 @@ import org.slf4j.LoggerFactory; /** - * Registers the security-agent violation counter with {@link SolrMetricManager} using a proper OTel - * label, keeping all {@code io.opentelemetry} types inside {@code solr:core} where they belong. + * Registers the security-agent violation counter with {@link SolrMetricManager}. * - *

    The agent JAR ({@code solr:agent-sm}) lives in the bootstrap classloader and has no OTel - * compile dependency. This class bridges the gap: it reads the raw {@code long} counts from {@code - * ViolationMetricsReporter} reflectively (trivial — only {@code long} primitives cross the - * boundary), and builds the full {@link Consumer}{@code <}{@link ObservableLongMeasurement}{@code - * >} callback using OTel types natively. - * - *

    A single OTel observable counter named {@code solr.security.agent.violations} is registered, - * with label {@code type=file|network|exit|exec}. In Prometheus format: + *

    The agent JAR ({@code solr:agent-sm}) has no OTel compile dependency; this class reads the raw + * {@code long} counts from {@code ViolationMetricsReporter} via reflection and builds the OTel + * callback natively. A single observable counter named {@code solr.security.agent.violations} is + * registered with label {@code type=file|network|exit|exec}. In Prometheus format: * *

      *   solr_security_agent_violations_total{type="file"}    N
    @@ -71,7 +66,6 @@ public static void register(SolrMetricManager metricManager, String registryName
           Method exitCount = reporter.getMethod("exitCount");
           Method execCount = reporter.getMethod("execCount");
     
    -      // Pre-build Attributes once — these are stable for the lifetime of the process.
           Attributes fileAttrs = Attributes.of(TYPE_KEY, "file");
           Attributes networkAttrs = Attributes.of(TYPE_KEY, "network");
           Attributes exitAttrs = Attributes.of(TYPE_KEY, "exit");
    @@ -98,7 +92,7 @@ public static void register(SolrMetricManager metricManager, String registryName
     
           log.debug("Security agent violation metrics registered under registry '{}'", registryName);
         } catch (ClassNotFoundException ignored) {
    -      // Agent JAR not loaded (e.g. SOLR_SECURITY_AGENT_SKIP=true) — nothing to register.
    +      // Agent JAR not loaded — nothing to register.
         } catch (Exception e) {
           log.warn("Failed to register security agent violation metrics", e);
         }
    
    From 860251b2ce10fbebe3d8c2da80aa38c990231acc Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= 
    Date: Thu, 28 May 2026 12:45:05 +0200
    Subject: [PATCH 42/65] Fix FileInterceptor enforcement bugs found in self code
     review
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    Three fixes from a self code review of the PR branch:
    
    1. Double violation count for open/newByteChannel with WRITE options:
       isMutating was set to true by the open/newByteChannel option inspection
       block, then the same path/action was checked again by the generic
       mutating block below — counting and logging the violation twice.
       Fixed by adding early return after the open/newByteChannel block.
    
    2. Files.move() destination not checked, source action wrong:
       Only args[0] (source) was checked, using "write" action. A move
       removes the source (requires "delete") and creates at the destination
       (requires "write"). Both are now checked.
    
    3. Unknown SocketAddress subclass silently bypassed network check:
       If args[0] was neither InetSocketAddress nor UnixDomainSocketAddress,
       SocketChannelInterceptor returned without checking. Now fails closed.
    
    Also refactored FileInterceptor to extract enforceFileAccess() so
    all enforcement paths — both the advice and the test helpers — share
    the same code instead of duplicating the counter/log/throw pattern.
    
    Adds 3 new tests and a "Known Limitations" section to security-agent.adoc
    documenting the java.io.*, async, and inbound-network gaps.
    ---
     .../solr/security/agent/FileInterceptor.java  | 175 ++++++++++++------
     .../agent/SocketChannelInterceptor.java       |  15 +-
     .../agent/SolrAgentIntegrationTest.java       |  53 ++++++
     .../pages/security-agent.adoc                 |  14 ++
     4 files changed, 195 insertions(+), 62 deletions(-)
    
    diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java
    index 94f12e16dc99..39a4e61b762f 100644
    --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java
    +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java
    @@ -72,8 +72,9 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin
         final String caller = walker.getCallerClass().getName();
     
         final String name = method.getName();
    -    boolean isMutating = name.equals("move") || name.equals("write") || name.startsWith("create");
    -    final boolean isDelete = isMutating == false ? name.startsWith("delete") : false;
    +    // "move" and "copy" are handled separately below (both endpoints must be checked).
    +    boolean isMutating = name.equals("write") || name.startsWith("create");
    +    final boolean isDelete = !isMutating && name.startsWith("delete");
     
         // This is Windows implementation of UNIX Domain Sockets (close)
         boolean isUnixSocketCaller = false;
    @@ -123,7 +124,7 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin
                       "Unsupported argument type: " + args[1].getClass().getName());
                 }
               }
    -        } else if (name.equals("copy") == true) {
    +        } else if (name.equals("copy") == true || name.equals("move") == true) {
               if (args.length > 1 && args[1] instanceof String pathStr) {
                 targetFilePath = Path.of(pathStr).toAbsolutePath().normalize().toString();
               } else if (args.length > 1 && args[1] instanceof Path path) {
    @@ -133,77 +134,85 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin
           }
     
           // Handle FileChannel.open() and newByteChannel() — check read/write permissions
    -      if (method.getName().equals("open") || method.getName().equals("newByteChannel")) {
    +      if (name.equals("open") || name.equals("newByteChannel")) {
             final String action = isMutating ? "write" : "read";
    -        if (!policy.isPathPermitted(filePath, action)) {
    -          ViolationMetricsReporter.incrementFile();
    -          SecurityViolationLogger.log(
    -              isMutating
    -                  ? SecurityViolationLogger.ViolationType.FILE_WRITE
    -                  : SecurityViolationLogger.ViolationType.FILE_READ,
    -              filePath,
    -              caller,
    -              policy.enforcementMode());
    -          if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) {
    -            throw new SecurityException(
    -                "Denied "
    -                    + (isMutating ? "OPEN (read/write)" : "OPEN (read)")
    -                    + " access to file: "
    -                    + filePath);
    -          }
    -        }
    +        enforceFileAccess(
    +            policy,
    +            filePath,
    +            action,
    +            isMutating
    +                ? SecurityViolationLogger.ViolationType.FILE_WRITE
    +                : SecurityViolationLogger.ViolationType.FILE_READ,
    +            caller,
    +            "Denied "
    +                + (isMutating ? "OPEN (read/write)" : "OPEN (read)")
    +                + " access to file: "
    +                + filePath);
    +        return; // fully handled; do not fall through
           }
     
    -      // Handle Files.copy() — check source read and target write permissions
    -      if (method.getName().equals("copy")) {
    -        if (!policy.isPathPermitted(filePath, "read")) {
    -          ViolationMetricsReporter.incrementFile();
    -          SecurityViolationLogger.log(
    -              SecurityViolationLogger.ViolationType.FILE_READ,
    -              filePath,
    +      // Handle Files.copy() — source requires read, destination requires write
    +      if (name.equals("copy")) {
    +        enforceFileAccess(
    +            policy,
    +            filePath,
    +            "read",
    +            SecurityViolationLogger.ViolationType.FILE_READ,
    +            caller,
    +            "Denied COPY (read) access to file: " + filePath);
    +        if (targetFilePath != null) {
    +          enforceFileAccess(
    +              policy,
    +              targetFilePath,
    +              "write",
    +              SecurityViolationLogger.ViolationType.FILE_WRITE,
                   caller,
    -              policy.enforcementMode());
    -          if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) {
    -            throw new SecurityException("Denied COPY (read) access to file: " + filePath);
    -          }
    +              "Denied COPY (write) access to file: " + targetFilePath);
             }
    -        if (targetFilePath != null && !policy.isPathPermitted(targetFilePath, "write")) {
    -          ViolationMetricsReporter.incrementFile();
    -          SecurityViolationLogger.log(
    -              SecurityViolationLogger.ViolationType.FILE_WRITE,
    +        return; // fully handled; do not fall through
    +      }
    +
    +      // Handle Files.move() — source requires delete, destination requires write
    +      if (name.equals("move")) {
    +        enforceFileAccess(
    +            policy,
    +            filePath,
    +            "delete",
    +            SecurityViolationLogger.ViolationType.FILE_DELETE,
    +            caller,
    +            "Denied MOVE (delete source) access to file: " + filePath);
    +        if (targetFilePath != null) {
    +          enforceFileAccess(
    +              policy,
                   targetFilePath,
    +              "write",
    +              SecurityViolationLogger.ViolationType.FILE_WRITE,
                   caller,
    -              policy.enforcementMode());
    -          if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) {
    -            throw new SecurityException("Denied COPY (write) access to file: " + targetFilePath);
    -          }
    +              "Denied MOVE (write destination) access to file: " + targetFilePath);
             }
    +        return; // fully handled; do not fall through
           }
     
    -      // File mutating operations
    -      if (isMutating && !policy.isPathPermitted(filePath, "write")) {
    -        ViolationMetricsReporter.incrementFile();
    -        SecurityViolationLogger.log(
    -            SecurityViolationLogger.ViolationType.FILE_WRITE,
    +      // Remaining mutating operations (write, createFile, createDirectories, createLink)
    +      if (isMutating) {
    +        enforceFileAccess(
    +            policy,
                 filePath,
    +            "write",
    +            SecurityViolationLogger.ViolationType.FILE_WRITE,
                 caller,
    -            policy.enforcementMode());
    -        if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) {
    -          throw new SecurityException("Denied WRITE access to file: " + filePath);
    -        }
    +            "Denied WRITE access to file: " + filePath);
           }
     
           // File deletion operations
    -      if (isDelete && !policy.isPathPermitted(filePath, "delete")) {
    -        ViolationMetricsReporter.incrementFile();
    -        SecurityViolationLogger.log(
    -            SecurityViolationLogger.ViolationType.FILE_DELETE,
    +      if (isDelete) {
    +        enforceFileAccess(
    +            policy,
                 filePath,
    +            "delete",
    +            SecurityViolationLogger.ViolationType.FILE_DELETE,
                 caller,
    -            policy.enforcementMode());
    -        if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) {
    -          throw new SecurityException("Denied DELETE access to file: " + filePath);
    -        }
    +            "Denied DELETE access to file: " + filePath);
           }
         }
       }
    @@ -243,8 +252,6 @@ public static String resolveRealPath(Path path) {
        * Checks whether {@code path} may be accessed with {@code action} under the active policy.
        * Increments the file violation counter and logs on violation; throws {@link SecurityException}
        * in enforce mode.
    -   *
    -   * 

    Used by tests to exercise the file-access check without ByteBuddy instrumentation. */ public static void checkPath( Path path, String action, SecurityViolationLogger.ViolationType violationType) { @@ -252,12 +259,58 @@ public static void checkPath( AgentPolicy policy = AgentPolicy.getInstance(); String resolvedPath = resolveRealPath(path); String caller = topCallerClassName(); + enforceFileAccess( + policy, + resolvedPath, + action, + violationType, + caller, + "Denied " + action.toUpperCase(Locale.ROOT) + " access to: " + resolvedPath); + } + + /** + * Checks whether a move from {@code source} to {@code target} is permitted under the active + * policy. Source requires "delete" permission; target requires "write" permission. + */ + public static void checkMove(Path source, Path target) { + if (!AgentPolicy.isInitialized()) return; + AgentPolicy policy = AgentPolicy.getInstance(); + String srcPath = resolveRealPath(source); + String dstPath = resolveRealPath(target); + String caller = topCallerClassName(); + enforceFileAccess( + policy, + srcPath, + "delete", + SecurityViolationLogger.ViolationType.FILE_DELETE, + caller, + "Denied MOVE (delete source) access to: " + srcPath); + enforceFileAccess( + policy, + dstPath, + "write", + SecurityViolationLogger.ViolationType.FILE_WRITE, + caller, + "Denied MOVE (write destination) access to: " + dstPath); + } + + /** + * Shared enforcement: checks whether the policy permits {@code action} on {@code resolvedPath}. + * Increments the file violation counter and logs; throws {@link SecurityException} in enforce + * mode. Used by both the {@link #intercept} advice and the test-side check helpers. + */ + static void enforceFileAccess( + AgentPolicy policy, + String resolvedPath, + String action, + SecurityViolationLogger.ViolationType violationType, + String caller, + String securityMessage) { if (!policy.isPathPermitted(resolvedPath, action)) { ViolationMetricsReporter.incrementFile(); SecurityViolationLogger.log(violationType, resolvedPath, caller, policy.enforcementMode()); if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { - throw new SecurityException( - "Denied " + action.toUpperCase(Locale.ROOT) + " access to: " + resolvedPath); + throw new SecurityException(securityMessage); } } } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java index 3ed0808e51f0..befb74989770 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java @@ -73,7 +73,20 @@ public static void intercept(@Advice.AllArguments Object[] args, @Origin Method } } else if (args[0] instanceof UnixDomainSocketAddress) { // Unix domain socket — local IPC, always allow - return; + } else if (args[0] != null) { + // Unknown SocketAddress subclass — fail closed + final String target = args[0].toString(); + ViolationMetricsReporter.incrementNetwork(); + SecurityViolationLogger.log( + SecurityViolationLogger.ViolationType.NETWORK_CONNECT, + target, + caller, + policy.enforcementMode()); + if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { + throw new SecurityException( + "Outbound network connection denied by Solr security agent (unknown address type): " + + target); + } } } diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java index b68cf48bb0f3..1bfa03a4168b 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java @@ -90,6 +90,59 @@ public void testDeniedFileReadIncrementsFileCounter() { assertEquals(before + 1, ViolationMetricsReporter.fileCount()); } + @Test + public void testMoveSourceRequiresDeleteNotWrite() { + Path tmpDir = createTempDir(); + // Grant write on destination but NOT delete on source + PermittedPath dstAllowed = + new PermittedPath(tmpDir.toString(), "write", true, PolicySource.DEFAULT); + buildEnforcePolicy(List.of(dstAllowed), List.of(), List.of(), List.of()); + + Path src = tmpDir.resolve("src.txt"); + Path dst = tmpDir.resolve("dst.txt"); + // Source lacks delete permission — should throw with a message about "delete source" + try { + FileInterceptor.checkMove(src, dst); + fail("Expected SecurityException"); + } catch (SecurityException e) { + assertTrue(e.getMessage(), e.getMessage().contains("delete source")); + } + } + + @Test + public void testMoveDestinationRequiresWrite() { + Path tmpDir = createTempDir(); + Path otherDir = createTempDir(); + // Grant delete on source dir, but NOT write on destination dir + PermittedPath srcAllowed = + new PermittedPath(tmpDir.toString(), "delete", true, PolicySource.DEFAULT); + buildEnforcePolicy(List.of(srcAllowed), List.of(), List.of(), List.of()); + + Path src = tmpDir.resolve("src.txt"); + Path dst = otherDir.resolve("dst.txt"); + try { + FileInterceptor.checkMove(src, dst); + fail("Expected SecurityException"); + } catch (SecurityException e) { + assertTrue(e.getMessage(), e.getMessage().contains("write destination")); + } + } + + @Test + public void testMoveCountsEachViolationOnce() { + long before = ViolationMetricsReporter.fileCount(); + buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); + Path src = Path.of("/forbidden/src.txt"); + Path dst = Path.of("/forbidden/dst.txt"); + // Source (delete) violation fires first and throws in enforce mode + try { + FileInterceptor.checkMove(src, dst); + } catch (SecurityException ignored) { + // expected + } + assertEquals(before + 1, ViolationMetricsReporter.fileCount()); + } + // --------------------------------------------------------------------------- // Network tests // --------------------------------------------------------------------------- diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc index a2e9eef7c541..bbf2cf49d6d4 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/security-agent.adoc @@ -168,6 +168,20 @@ The following Solr modules have wildcard outbound network access pre-permitted i Other modules (such as `extraction`) require explicit policy entries when connecting to external services. See the module's documentation for details. +== Known Limitations + +The agent intercepts the standard NIO file APIs (`FileSystemProvider`, `java.nio.file.Files`, `FileChannel`). +The following Java APIs are *not* instrumented and will bypass policy enforcement: + +* `java.io.FileInputStream`, `java.io.FileOutputStream`, `java.io.RandomAccessFile` — legacy stream I/O uses native `open0()` and does not go through `FileSystemProvider`. +* `java.nio.channels.AsynchronousFileChannel` — async file I/O is not instrumented. +* `java.net.ServerSocket` — inbound TCP socket creation is not checked; only outbound `connect()` calls are intercepted. +* `java.nio.channels.DatagramChannel` — UDP send/receive is not checked. +* `java.nio.channels.AsynchronousSocketChannel` — async socket operations are not checked. + +These gaps reflect the current scope of the agent, which targets the primary I/O paths used by Solr and the JVM internals. +Operators requiring tighter isolation should supplement the agent with OS-level controls (e.g., Linux seccomp, SELinux, or container security profiles). + == See Also * xref:configuration-guide:solr-properties.adoc[] — `solr.security.agent.*` property reference From 9f581a22aec3932ac369c320a619e1aa0ce28f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 12:52:08 +0200 Subject: [PATCH 43/65] Move test-only check helpers out of production interceptors checkPath, checkMove, checkConnect, topCallerClassName, resolveRealPath were public methods on FileInterceptor and SocketChannelInterceptor only ever called from tests. Moved them to a new package-private InterceptorTestHelper class in the test source set. Production code now exposes only the shared enforcement methods: - FileInterceptor.enforceFileAccess() - SocketChannelInterceptor.enforceNetworkAccess() These are the same methods the live @Advice intercept() paths call, so tests exercise the same code path as production. --- .../solr/security/agent/FileInterceptor.java | 79 ------------- .../agent/SocketChannelInterceptor.java | 52 ++------ .../security/agent/InterceptorTestHelper.java | 111 ++++++++++++++++++ .../agent/SocketChannelInterceptorTest.java | 2 +- .../agent/SolrAgentIntegrationTest.java | 18 +-- .../security/agent/SymlinkEscapeTest.java | 12 +- 6 files changed, 138 insertions(+), 136 deletions(-) create mode 100644 solr/agent-sm/src/test/org/apache/solr/security/agent/InterceptorTestHelper.java diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java index 39a4e61b762f..65af1d198475 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java @@ -17,13 +17,11 @@ package org.apache.solr.security.agent; import java.lang.reflect.Method; -import java.nio.file.LinkOption; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.nio.file.spi.FileSystemProvider; import java.util.Collection; -import java.util.Locale; import java.util.Set; import net.bytebuddy.asm.Advice; @@ -217,83 +215,6 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin } } - // --------------------------------------------------------------------------- - // Static helpers (used by advice and by tests) - // --------------------------------------------------------------------------- - - public static String topCallerClassName() { - try { - return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) - .getCallerClass() - .getName(); - } catch (Exception e) { - return ""; - } - } - - /** - * Resolves the real path of {@code path}, following symlinks. Falls back to {@code - * normalize().toAbsolutePath()} if the file does not exist or if an I/O error occurs. - * - *

    Note: This method must NOT be called from the ByteBuddy {@link #intercept} advice - * method — {@code toRealPath()} performs file-system I/O which would trigger re-entrant - * interception and cause infinite recursion. It is safe to use only from the test-side {@link - * #checkPath} helper where no live instrumentation is active. - */ - public static String resolveRealPath(Path path) { - try { - return path.toRealPath(new LinkOption[0]).toString(); - } catch (Exception e) { - return path.normalize().toAbsolutePath().toString(); - } - } - - /** - * Checks whether {@code path} may be accessed with {@code action} under the active policy. - * Increments the file violation counter and logs on violation; throws {@link SecurityException} - * in enforce mode. - */ - public static void checkPath( - Path path, String action, SecurityViolationLogger.ViolationType violationType) { - if (!AgentPolicy.isInitialized()) return; - AgentPolicy policy = AgentPolicy.getInstance(); - String resolvedPath = resolveRealPath(path); - String caller = topCallerClassName(); - enforceFileAccess( - policy, - resolvedPath, - action, - violationType, - caller, - "Denied " + action.toUpperCase(Locale.ROOT) + " access to: " + resolvedPath); - } - - /** - * Checks whether a move from {@code source} to {@code target} is permitted under the active - * policy. Source requires "delete" permission; target requires "write" permission. - */ - public static void checkMove(Path source, Path target) { - if (!AgentPolicy.isInitialized()) return; - AgentPolicy policy = AgentPolicy.getInstance(); - String srcPath = resolveRealPath(source); - String dstPath = resolveRealPath(target); - String caller = topCallerClassName(); - enforceFileAccess( - policy, - srcPath, - "delete", - SecurityViolationLogger.ViolationType.FILE_DELETE, - caller, - "Denied MOVE (delete source) access to: " + srcPath); - enforceFileAccess( - policy, - dstPath, - "write", - SecurityViolationLogger.ViolationType.FILE_WRITE, - caller, - "Denied MOVE (write destination) access to: " + dstPath); - } - /** * Shared enforcement: checks whether the policy permits {@code action} on {@code resolvedPath}. * Increments the file violation counter and logs; throws {@link SecurityException} in enforce diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java index befb74989770..0aa71de4f37e 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java @@ -54,27 +54,12 @@ public static void intercept(@Advice.AllArguments Object[] args, @Origin Method if (args[0] instanceof InetSocketAddress address) { if (!policy.trustedHosts().contains(address.getHostString())) { - final String host = address.getHostString(); - final int port = address.getPort(); - - if (!isEndpointPermitted(policy, host, port)) { - final String target = host + ":" + port; - ViolationMetricsReporter.incrementNetwork(); - SecurityViolationLogger.log( - SecurityViolationLogger.ViolationType.NETWORK_CONNECT, - target, - caller, - policy.enforcementMode()); - if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { - throw new SecurityException( - "Outbound network connection denied by Solr security agent: " + target); - } - } + enforceNetworkAccess(policy, address.getHostString(), address.getPort(), caller); } } else if (args[0] instanceof UnixDomainSocketAddress) { // Unix domain socket — local IPC, always allow } else if (args[0] != null) { - // Unknown SocketAddress subclass — fail closed + // Unknown SocketAddress subclass — fail closed (host/port unknown, cannot consult policy) final String target = args[0].toString(); ViolationMetricsReporter.incrementNetwork(); SecurityViolationLogger.log( @@ -84,31 +69,22 @@ public static void intercept(@Advice.AllArguments Object[] args, @Origin Method policy.enforcementMode()); if (policy.enforcementMode() == AgentPolicy.EnforcementMode.ENFORCE) { throw new SecurityException( - "Outbound network connection denied by Solr security agent (unknown address type): " - + target); + "Outbound network connection denied (unknown address type): " + target); } } } // --------------------------------------------------------------------------- - // Static helpers (used by advice and by tests) + // Shared enforcement helper // --------------------------------------------------------------------------- /** - * Checks whether the given remote address may be connected to under the active policy. Increments - * the network violation counter and logs on violation; throws {@link SecurityException} in - * enforce mode. - * - *

    Used by tests to exercise the network check without ByteBuddy instrumentation. + * Enforces the network policy for {@code host:port}. Increments the network violation counter, + * logs, and throws {@link SecurityException} in enforce mode if no permitted endpoint matches. + * Used by both the {@link #intercept} advice and the test-side helper in {@code + * InterceptorTestHelper}. */ - public static void checkConnect(InetSocketAddress address) { - if (!AgentPolicy.isInitialized()) return; - if (address.isUnresolved()) return; - AgentPolicy policy = AgentPolicy.getInstance(); - if (policy.trustedHosts().contains(address.getHostString())) return; - String caller = topCallerClassName(); - String host = address.getHostString(); - int port = address.getPort(); + static void enforceNetworkAccess(AgentPolicy policy, String host, int port, String caller) { if (!isEndpointPermitted(policy, host, port)) { String target = host + ":" + port; ViolationMetricsReporter.incrementNetwork(); @@ -124,16 +100,6 @@ public static void checkConnect(InetSocketAddress address) { } } - public static String topCallerClassName() { - try { - return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) - .getCallerClass() - .getName(); - } catch (Exception e) { - return ""; - } - } - // --------------------------------------------------------------------------- // Endpoint matching helpers // --------------------------------------------------------------------------- diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/InterceptorTestHelper.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/InterceptorTestHelper.java new file mode 100644 index 000000000000..b56b922b36ab --- /dev/null +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/InterceptorTestHelper.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security.agent; + +import java.net.InetSocketAddress; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Locale; + +/** + * Test-only helpers that exercise interceptor enforcement logic without ByteBuddy instrumentation. + * All enforcement is delegated to the same shared methods used by the live advice. + */ +class InterceptorTestHelper { + + private InterceptorTestHelper() {} + + /** + * Resolves {@code path} to its real path (following symlinks), falling back to {@code + * normalize().toAbsolutePath()} on I/O error. + */ + static String resolveRealPath(Path path) { + try { + return path.toRealPath(new LinkOption[0]).toString(); + } catch (Exception e) { + return path.normalize().toAbsolutePath().toString(); + } + } + + /** Returns the name of the calling class for use as the {@code caller} field in violations. */ + static String callerClassName() { + try { + return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .getCallerClass() + .getName(); + } catch (Exception e) { + return ""; + } + } + + /** + * Checks whether {@code path} may be accessed with {@code action} under the active policy. + * Delegates to {@link FileInterceptor#enforceFileAccess}. + */ + static void checkPath(Path path, String action, SecurityViolationLogger.ViolationType type) { + if (!AgentPolicy.isInitialized()) return; + AgentPolicy policy = AgentPolicy.getInstance(); + String resolved = resolveRealPath(path); + FileInterceptor.enforceFileAccess( + policy, + resolved, + action, + type, + callerClassName(), + "Denied " + action.toUpperCase(Locale.ROOT) + " access to: " + resolved); + } + + /** + * Checks whether a move from {@code source} to {@code target} is permitted. Source requires + * "delete"; target requires "write". Delegates to {@link FileInterceptor#enforceFileAccess}. + */ + static void checkMove(Path source, Path target) { + if (!AgentPolicy.isInitialized()) return; + AgentPolicy policy = AgentPolicy.getInstance(); + String srcPath = resolveRealPath(source); + String dstPath = resolveRealPath(target); + String caller = callerClassName(); + FileInterceptor.enforceFileAccess( + policy, + srcPath, + "delete", + SecurityViolationLogger.ViolationType.FILE_DELETE, + caller, + "Denied MOVE (delete source) access to: " + srcPath); + FileInterceptor.enforceFileAccess( + policy, + dstPath, + "write", + SecurityViolationLogger.ViolationType.FILE_WRITE, + caller, + "Denied MOVE (write destination) access to: " + dstPath); + } + + /** + * Checks whether connecting to {@code address} is permitted under the active policy. Delegates to + * {@link SocketChannelInterceptor#enforceNetworkAccess}. + */ + static void checkConnect(InetSocketAddress address) { + if (!AgentPolicy.isInitialized()) return; + if (address.isUnresolved()) return; + AgentPolicy policy = AgentPolicy.getInstance(); + if (policy.trustedHosts().contains(address.getHostString())) return; + String host = address.getHostString(); + int port = address.getPort(); + SocketChannelInterceptor.enforceNetworkAccess(policy, host, port, callerClassName()); + } +} diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java index 03cf18168eef..9a87cb2c54f3 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java @@ -57,7 +57,7 @@ public void testLoopbackPermittedViaTrustedHosts() throws Exception { InetSocketAddress loopback = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 8983); assertTrue(policy.trustedHosts().contains(loopback.getHostString())); // checkConnect skips trusted hosts before policy lookup - SocketChannelInterceptor.checkConnect(loopback); // must not throw + InterceptorTestHelper.checkConnect(loopback); // must not throw } @Test diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java index 1bfa03a4168b..8ab455fd2c5b 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SolrAgentIntegrationTest.java @@ -64,8 +64,8 @@ public void testPermittedFileReadSucceeds() { buildEnforcePolicy(List.of(allowed), List.of(), List.of(), List.of()); Path target = tmpDir.resolve("test.txt"); - // checkPath should not throw for a path inside the permitted dir - FileInterceptor.checkPath(target, "read", SecurityViolationLogger.ViolationType.FILE_READ); + InterceptorTestHelper.checkPath( + target, "read", SecurityViolationLogger.ViolationType.FILE_READ); } @Test(expected = SecurityException.class) @@ -73,7 +73,7 @@ public void testDeniedFileReadThrows() { Path tmpDir = createTempDir(); // Policy permits nothing buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); - FileInterceptor.checkPath( + InterceptorTestHelper.checkPath( tmpDir.resolve("secret.txt"), "read", SecurityViolationLogger.ViolationType.FILE_READ); } @@ -82,7 +82,7 @@ public void testDeniedFileReadIncrementsFileCounter() { long before = ViolationMetricsReporter.fileCount(); buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); try { - FileInterceptor.checkPath( + InterceptorTestHelper.checkPath( Path.of("/etc/passwd"), "read", SecurityViolationLogger.ViolationType.FILE_READ); } catch (SecurityException ignored) { // expected @@ -102,7 +102,7 @@ public void testMoveSourceRequiresDeleteNotWrite() { Path dst = tmpDir.resolve("dst.txt"); // Source lacks delete permission — should throw with a message about "delete source" try { - FileInterceptor.checkMove(src, dst); + InterceptorTestHelper.checkMove(src, dst); fail("Expected SecurityException"); } catch (SecurityException e) { assertTrue(e.getMessage(), e.getMessage().contains("delete source")); @@ -121,7 +121,7 @@ public void testMoveDestinationRequiresWrite() { Path src = tmpDir.resolve("src.txt"); Path dst = otherDir.resolve("dst.txt"); try { - FileInterceptor.checkMove(src, dst); + InterceptorTestHelper.checkMove(src, dst); fail("Expected SecurityException"); } catch (SecurityException e) { assertTrue(e.getMessage(), e.getMessage().contains("write destination")); @@ -136,7 +136,7 @@ public void testMoveCountsEachViolationOnce() { Path dst = Path.of("/forbidden/dst.txt"); // Source (delete) violation fires first and throws in enforce mode try { - FileInterceptor.checkMove(src, dst); + InterceptorTestHelper.checkMove(src, dst); } catch (SecurityException ignored) { // expected } @@ -160,7 +160,7 @@ public void testPermittedEndpointNotBlocked() { public void testDeniedNetworkConnectThrowsInEnforceMode() throws Exception { buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); InetSocketAddress addr = new InetSocketAddress(InetAddress.getByName("10.0.0.99"), 9999); - SocketChannelInterceptor.checkConnect(addr); + InterceptorTestHelper.checkConnect(addr); } @Test @@ -169,7 +169,7 @@ public void testDeniedNetworkIncrementsCounter() throws Exception { buildEnforcePolicy(List.of(), List.of(), List.of(), List.of()); InetSocketAddress addr = new InetSocketAddress(InetAddress.getByName("10.0.0.99"), 9999); try { - SocketChannelInterceptor.checkConnect(addr); + InterceptorTestHelper.checkConnect(addr); } catch (SecurityException ignored) { // expected } diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java index aee886c0878d..d9efad14d0c0 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SymlinkEscapeTest.java @@ -84,7 +84,8 @@ public void testSymlinkInsidePermittedDirIsAllowed() throws Exception { } // Symlink resolves to a path inside the permitted dir — should NOT throw - FileInterceptor.checkPath(symlink, "read", SecurityViolationLogger.ViolationType.FILE_READ); + InterceptorTestHelper.checkPath( + symlink, "read", SecurityViolationLogger.ViolationType.FILE_READ); } // --------------------------------------------------------------------------- @@ -113,7 +114,8 @@ public void testSymlinkEscapeBlockedInEnforceMode() throws Exception { // Accessing via the symlink path should be denied because the REAL path is outside permitted // dir - FileInterceptor.checkPath(symlink, "read", SecurityViolationLogger.ViolationType.FILE_READ); + InterceptorTestHelper.checkPath( + symlink, "read", SecurityViolationLogger.ViolationType.FILE_READ); } @Test @@ -135,7 +137,8 @@ public void testSymlinkEscapeIncrementsFileCounter() throws Exception { long before = ViolationMetricsReporter.fileCount(); try { - FileInterceptor.checkPath(symlink, "read", SecurityViolationLogger.ViolationType.FILE_READ); + InterceptorTestHelper.checkPath( + symlink, "read", SecurityViolationLogger.ViolationType.FILE_READ); } catch (SecurityException ignored) { // expected } @@ -154,6 +157,7 @@ public void testNonExistentPathFallsBackToNormalizedCheck() { // Non-existent file inside the permitted dir — toRealPath() fails → normalized path used Path nonExistent = tmpDir.resolve("does-not-exist.txt"); // Should NOT throw — the normalized path falls within the permitted dir - FileInterceptor.checkPath(nonExistent, "read", SecurityViolationLogger.ViolationType.FILE_READ); + InterceptorTestHelper.checkPath( + nonExistent, "read", SecurityViolationLogger.ViolationType.FILE_READ); } } From 1689967d2f0dbcd0d4e5fafe8ebb907bea3b80e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 12:56:24 +0200 Subject: [PATCH 44/65] Fix BATS metric assertion to not depend on OTel label order The Prometheus output includes OTel scope labels alongside the type label (e.g. otel_scope_name="org.apache.solr",type="file"), so asserting on 'solr_security_agent_violations_total{type="file"' fails. Assert on the base metric name only since the presence of the counter is what matters. --- solr/packaging/test/test_security_agent.bats | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/solr/packaging/test/test_security_agent.bats b/solr/packaging/test/test_security_agent.bats index b487a390bbe5..a3f152f9a2dd 100644 --- a/solr/packaging/test/test_security_agent.bats +++ b/solr/packaging/test/test_security_agent.bats @@ -60,11 +60,11 @@ console_log() { # Confirms: CoreContainer reflective call to ViolationMetricsReporter registered counters. # A single labeled counter solr.security.agent.violations appears in Prometheus format as - # solr_security_agent_violations_total{type="file"|"network"|"exit"|"exec"}. + # solr_security_agent_violations_total{...type="file"...} etc. OTel may add extra labels + # (e.g. otel_scope_name), so we assert on the base metric name only. run curl -sf "http://localhost:${SOLR_PORT}/solr/admin/metrics" assert_success - assert_output --partial 'solr_security_agent_violations_total{type="file"' - assert_output --partial 'solr_security_agent_violations_total{type="network"' + assert_output --partial 'solr_security_agent_violations_total' } @test "SOLR_SECURITY_AGENT_SKIP=true disables the agent" { From 43cdf134ce8f42d273257dcbe9f2b6db70939577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 12:57:07 +0200 Subject: [PATCH 45/65] Treat unknown OpenOption argument type as mutating instead of throwing Throwing SecurityException unconditionally for an unrecognised args[1] type bypassed the violation counter and structured log, and would break legitimate file operations in warn mode. Unknown types are now treated conservatively as mutating (write check applied). --- .../java/org/apache/solr/security/agent/FileInterceptor.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java index 65af1d198475..555754123cb8 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java @@ -118,8 +118,7 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin } } } else { - throw new SecurityException( - "Unsupported argument type: " + args[1].getClass().getName()); + isMutating = true; // unknown option type — treat conservatively as mutating } } } else if (name.equals("copy") == true || name.equals("move") == true) { From 1ee6cd7f3f964f63662bd00afe4f97ef4a45e212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 12:57:45 +0200 Subject: [PATCH 46/65] Fix AgentPolicy Javadoc: class-name matching is prefix/exact, not regex The Javadoc claimed String#matches (full regex) with patterns like 'org\.apache\.solr\..*'. The actual implementation in ApprovedCallSite.matches() uses equals/startsWith semantics: '*' is wildcard, 'pkg.*' matches the package and sub-packages, anything else is an exact class-name match. --- .../src/java/org/apache/solr/security/agent/AgentPolicy.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java index 219cdf631c9e..c4446f300919 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java @@ -244,8 +244,9 @@ public boolean isExecApproved(String className) { * Returns {@code true} if any class in the call chain is approved to call {@code System.exit()} * or {@code Runtime.halt()}. Any approved class anywhere in the chain grants permission. * - *

    Class names are matched using {@link String#matches} (full regex), so the approved-caller - * list supports wildcard patterns such as {@code "org\\.apache\\.solr\\..*"}. + *

    Class names are matched by {@link ApprovedCallSite#matches}: {@code "*"} matches any class; + * a pattern ending in {@code ".*"} matches the named package and all its sub-packages; anything + * else is an exact class-name match. * * @param chain the full set of non-hidden classes in the call stack */ From 95eeed270634f5ea0a3cb6ab78a72ba383e3a3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 13:00:30 +0200 Subject: [PATCH 47/65] Fix isCallerFromCodeBase: normalize path separators for Windows On Windows, CodeSource.getLocation().getPath() may return backslash separators while the codeBase string from the policy file uses forward slashes. Normalize both strings to forward slashes before the startsWith/equals comparison so the recursive codeBase check works correctly on all platforms. --- .../solr/security/agent/SocketChannelInterceptor.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java index 0aa71de4f37e..14bd6a60c844 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java @@ -172,10 +172,13 @@ static boolean isCallerFromCodeBase(Collection> chain, String codeBase) while (locPath.endsWith("/") || locPath.endsWith("\\")) locPath = locPath.substring(0, locPath.length() - 1); + // Normalise path separators for cross-platform comparison (Windows may use backslashes) + String normBase = base.replace('\\', '/'); + String normLocPath = locPath.replace('\\', '/'); if (recursive) { - if (locPath.equals(base) || locPath.startsWith(base + "/")) return true; + if (normLocPath.equals(normBase) || normLocPath.startsWith(normBase + "/")) return true; } else { - if (locPath.equals(base)) return true; + if (normLocPath.equals(normBase)) return true; } } catch (Exception ignored) { // SecurityException or other runtime exception — skip this frame From 5e33e83373539edcfcc3824dec4b70016115c49a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 13:03:46 +0200 Subject: [PATCH 48/65] Fix FileInterceptor: resolve symlinks via toRealPath() in advice The interceptor was using toAbsolutePath().normalize() which does not follow symbolic links, allowing a symlink inside a permitted directory that points outside it to bypass the policy check. Replace with resolveRealPath() which calls toRealPath() to resolve the real on-disk path before the policy check. A ThreadLocal guard (IN_SYMLINK_RESOLVE) prevents infinite recursion in case toRealPath() itself triggers an intercepted file operation. On IOException (path does not yet exist), falls back to toAbsolutePath().normalize(). The same fix is applied to the copy/move target-path resolution. --- .../solr/security/agent/FileInterceptor.java | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java index 555754123cb8..ee7d727ded6a 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java @@ -16,6 +16,7 @@ */ package org.apache.solr.security.agent; +import java.io.IOException; import java.lang.reflect.Method; import java.nio.file.OpenOption; import java.nio.file.Path; @@ -36,6 +37,34 @@ public class FileInterceptor { /** FileInterceptor */ public FileInterceptor() {} + /** + * Guard to prevent re-entrant calls: set to {@code true} while symlink resolution is in progress + * on the current thread, so that any file operation triggered by {@code toRealPath()} is not + * intercepted again (which would cause infinite recursion). + */ + static final ThreadLocal IN_SYMLINK_RESOLVE = ThreadLocal.withInitial(() -> false); + + /** + * Resolves a path to its real, symlink-free form for policy checks. Falls back to {@link + * Path#toAbsolutePath() toAbsolutePath().normalize()} when (a) the path does not exist yet, or + * (b) {@code toRealPath()} itself triggers a re-entrant interception (detected via {@link + * #IN_SYMLINK_RESOLVE}). + */ + static String resolveRealPath(Path path) { + if (Boolean.TRUE.equals(IN_SYMLINK_RESOLVE.get())) { + return path.toAbsolutePath().normalize().toString(); + } + IN_SYMLINK_RESOLVE.set(true); + try { + return path.toRealPath().toString(); + } catch (IOException e) { + // Path does not exist yet — fall back to normalized path + return path.toAbsolutePath().normalize().toString(); + } finally { + IN_SYMLINK_RESOLVE.set(false); + } + } + /** * Intercepts file operations. * @@ -54,7 +83,7 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin if (args.length > 0 && args[0] instanceof String pathStr) { filePath = Path.of(pathStr).toAbsolutePath().normalize().toString(); } else if (args.length > 0 && args[0] instanceof Path path) { - filePath = path.toAbsolutePath().normalize().toString(); + filePath = resolveRealPath(path); provider = path.getFileSystem().provider(); } @@ -125,7 +154,7 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin if (args.length > 1 && args[1] instanceof String pathStr) { targetFilePath = Path.of(pathStr).toAbsolutePath().normalize().toString(); } else if (args.length > 1 && args[1] instanceof Path path) { - targetFilePath = path.toAbsolutePath().normalize().toString(); + targetFilePath = resolveRealPath(path); } } } From 1db48be42609acd3532d5cfcd02f4e44ed48fd81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 13:26:39 +0200 Subject: [PATCH 49/65] Drop unused @Origin Method param from SocketChannelInterceptor.intercept ByteBuddy binds @Origin Method on every intercepted connect() call even though the parameter was never read. Removing it eliminates a reflective Method lookup on each outbound connection attempt. --- .../security/agent/SocketChannelInterceptor.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java index 14bd6a60c844..02ba0336335a 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java @@ -16,14 +16,12 @@ */ package org.apache.solr.security.agent; -import java.lang.reflect.Method; import java.net.InetSocketAddress; import java.net.URL; import java.net.UnixDomainSocketAddress; import java.security.CodeSource; import java.util.Collection; import net.bytebuddy.asm.Advice; -import net.bytebuddy.asm.Advice.Origin; /** * ByteBuddy {@link Advice} interceptor for outbound socket connections. @@ -37,15 +35,13 @@ public class SocketChannelInterceptor { public SocketChannelInterceptor() {} /** - * Interceptors + * Intercepts outbound socket connections. * - * @param args arguments - * @param method method - * @throws Exception exceptions + * @param args arguments of the intercepted {@code connect} method + * @throws Exception if the connection is denied in enforce mode */ @Advice.OnMethodEnter - public static void intercept(@Advice.AllArguments Object[] args, @Origin Method method) - throws Exception { + public static void intercept(@Advice.AllArguments Object[] args) throws Exception { if (!AgentPolicy.isInitialized()) return; final AgentPolicy policy = AgentPolicy.getInstance(); From 69774c5ddf843b4045c818f2e9f49e373697c1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 13:29:41 +0200 Subject: [PATCH 50/65] Fix codeBase-scoped exitVM/exec grants being silently no-ops When a policy grant block included a codeBase attribute, e.g.: grant codeBase "file:/opt/solr/modules/foo/-" { permission java.lang.RuntimePermission "exitVM"; }; PolicyLoader was storing the codeBase URL string as the classNamePattern in ApprovedCallSite. ApprovedCallSite.matches() then compared it against fully-qualified class names, which can never match a "file:/.../..." URL string, making all codeBase-scoped exit/exec grants silently no-ops. Fix: ApprovedCallSite now carries a separate codeBase field. When codeBase is set, matchesCodeBase(Class) is used (delegating to the same SocketChannelInterceptor.isCallerFromCodeBase logic used for network grants). AgentPolicy.isChainThatCanExit/Exec dispatch to the right matcher depending on which field is set. The simple isExitApproved/isExecApproved(String) helpers skip codeBase entries since they have no Class to inspect. Also correct the ApprovedCallSite Javadoc (previously claimed SolrCLI and SolrDispatchFilter are default-approved exit callers, which was never true), and add an explanatory comment in agent-security.policy documenting why exitVM grants are intentionally absent from the default policy. --- .../solr/security/agent/AgentPolicy.java | 16 +++-- .../solr/security/agent/ApprovedCallSite.java | 70 +++++++++++++++---- .../solr/security/agent/PolicyLoader.java | 16 +++-- solr/server/etc/agent-security.policy | 16 +++++ 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java index c4446f300919..10e15e3b6397 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java @@ -224,7 +224,10 @@ public boolean isPathPermitted(String resolvedPath, String action) { */ public boolean isExitApproved(String className) { for (ApprovedCallSite cs : approvedExitCallers) { - if (cs.operation() == ApprovedCallSite.Operation.EXIT && cs.matches(className)) return true; + // codeBase entries require a Class object; skip them here + if (cs.operation() == ApprovedCallSite.Operation.EXIT + && cs.codeBase() == null + && cs.matches(className)) return true; } return false; } @@ -235,7 +238,10 @@ public boolean isExitApproved(String className) { */ public boolean isExecApproved(String className) { for (ApprovedCallSite cs : approvedExecCallers) { - if (cs.operation() == ApprovedCallSite.Operation.EXEC && cs.matches(className)) return true; + // codeBase entries require a Class object; skip them here + if (cs.operation() == ApprovedCallSite.Operation.EXEC + && cs.codeBase() == null + && cs.matches(className)) return true; } return false; } @@ -253,7 +259,8 @@ public boolean isExecApproved(String className) { public boolean isChainThatCanExit(Collection> chain) { for (Class cls : chain) { for (ApprovedCallSite cs : approvedExitCallers) { - if (cs.operation() == ApprovedCallSite.Operation.EXIT && cs.matches(cls.getName())) { + if (cs.operation() != ApprovedCallSite.Operation.EXIT) continue; + if (cs.codeBase() != null ? cs.matchesCodeBase(cls) : cs.matches(cls.getName())) { return true; } } @@ -272,7 +279,8 @@ public boolean isChainThatCanExit(Collection> chain) { public boolean isChainThatCanExec(Collection> chain) { for (Class cls : chain) { for (ApprovedCallSite cs : approvedExecCallers) { - if (cs.operation() == ApprovedCallSite.Operation.EXEC && cs.matches(cls.getName())) { + if (cs.operation() != ApprovedCallSite.Operation.EXEC) continue; + if (cs.codeBase() != null ? cs.matchesCodeBase(cls) : cs.matches(cls.getName())) { return true; } } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java index 37d8b33a4aee..d7fee41b8406 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java @@ -16,20 +16,27 @@ */ package org.apache.solr.security.agent; +import java.util.List; + /** - * A class (or class-name prefix pattern) that is approved to perform a restricted operation such as - * calling {@code System.exit()} or spawning a child process via {@code ProcessBuilder}. + * A call-site pattern that is approved to perform a restricted operation such as calling {@code + * System.exit()} or spawning a child process via {@code ProcessBuilder}. + * + *

    Approved call sites are loaded from the security policy at startup and are immutable. Each + * entry matches callers either by class-name pattern or by code-source location (codeBase). * - *

    Approved call sites are loaded from the security policy at startup and are immutable. Entries - * use either an exact fully-qualified class name or a prefix ending in {@code .*} (e.g. {@code - * org.apache.solr.cli.*}). + *

    Class-name matching (when {@link #codeBase()} is {@code null}): uses the same syntax as + * {@link AgentPolicy#isChainThatCanExit}: {@code "*"} matches any class; a pattern ending in {@code + * ".*"} (e.g. {@code org.apache.solr.cli.*}) matches that package and all sub-packages; anything + * else is an exact fully-qualified class name. * - *

    Default approved EXIT callers: + *

    codeBase matching (when {@link #codeBase()} is non-{@code null}): the calling class + * must have been loaded from a JAR or directory matching the codeBase URL, using the same syntax as + * JDK policy files ({@code file:/path/-} for recursive, {@code file:/path/to.jar} for exact). * - *

      - *
    • {@code org.apache.solr.cli.SolrCLI} - *
    • {@code org.apache.solr.servlet.SolrDispatchFilter} - *
    + *

    Default approved EXIT callers: none (empty list in the bundled production policy). Operators + * can add entries via {@code agent-security-extra.policy} when specific code needs to call {@code + * System.exit()} — for example, a custom lifecycle plugin. * *

    Default approved EXEC callers: none (empty list in production policy). */ @@ -41,21 +48,44 @@ public enum Operation { EXEC } - private final String classNamePattern; + private final String classNamePattern; // null when codeBase matching is used + private final String codeBase; // null when class-name matching is used private final Operation operation; private final PolicySource source; + /** Constructs a class-name–based approval (no codeBase constraint). */ ApprovedCallSite(String classNamePattern, Operation operation, PolicySource source) { + this(classNamePattern, null, operation, source); + } + + /** + * Constructs an approval that is either class-name–based ({@code codeBase == null}) or + * code-source–based ({@code classNamePattern == null}). + */ + ApprovedCallSite( + String classNamePattern, String codeBase, Operation operation, PolicySource source) { this.classNamePattern = classNamePattern; + this.codeBase = codeBase; this.operation = operation; this.source = source; } - /** Fully-qualified class name or prefix pattern (ending in {@code .*}). */ + /** + * Fully-qualified class name or prefix pattern (ending in {@code .*}), or {@code null} when this + * entry uses codeBase matching instead. + */ public String classNamePattern() { return classNamePattern; } + /** + * JDK-style codeBase URL for code-source matching (e.g. {@code file:/opt/solr/modules/foo/-}), or + * {@code null} when this entry uses class-name matching instead. + */ + public String codeBase() { + return codeBase; + } + /** The restricted operation this approval covers. */ public Operation operation() { return operation; @@ -67,10 +97,12 @@ public PolicySource source() { } /** - * Returns {@code true} if the given fully-qualified class name matches this approved call-site - * pattern. + * Returns {@code true} if the given fully-qualified class name matches this entry's class-name + * pattern. Always returns {@code false} when this entry uses codeBase matching — use {@link + * #matchesCodeBase(Class)} instead. */ public boolean matches(String className) { + if (codeBase != null) return false; // codeBase entries must be checked via matchesCodeBase if ("*".equals(classNamePattern)) return true; if (classNamePattern.endsWith(".*")) { String prefix = classNamePattern.substring(0, classNamePattern.length() - 2); @@ -78,4 +110,14 @@ public boolean matches(String className) { } return classNamePattern.equals(className); } + + /** + * Returns {@code true} if this entry has a codeBase constraint and the given class was loaded + * from a code source matching that codeBase. Always returns {@code false} when this entry uses + * class-name matching. + */ + public boolean matchesCodeBase(Class cls) { + if (codeBase == null) return false; + return SocketChannelInterceptor.isCallerFromCodeBase(List.of(cls), codeBase); + } } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java index c7a347b29328..5320c7b7cda2 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java @@ -215,14 +215,20 @@ private AgentPolicy buildPolicy(List grants) { } for (RawRuntimePermission rp : block.runtimePerms) { if ("exitVM".equals(rp.type)) { - // codeBase-scoped exitVM grants map to approved exit callers - String pattern = rp.codeBase != null ? rp.codeBase : "*"; + // Grant without codeBase: "*" (any class may exit); with codeBase: match by code source exitCallers.add( - new ApprovedCallSite(pattern, ApprovedCallSite.Operation.EXIT, rp.source)); + new ApprovedCallSite( + rp.codeBase != null ? null : "*", + rp.codeBase, + ApprovedCallSite.Operation.EXIT, + rp.source)); } else if ("exec".equals(rp.type)) { - String pattern = rp.codeBase != null ? rp.codeBase : "*"; execCallers.add( - new ApprovedCallSite(pattern, ApprovedCallSite.Operation.EXEC, rp.source)); + new ApprovedCallSite( + rp.codeBase != null ? null : "*", + rp.codeBase, + ApprovedCallSite.Operation.EXEC, + rp.source)); } } } diff --git a/solr/server/etc/agent-security.policy b/solr/server/etc/agent-security.policy index 9b0e583b52ef..4fa7b3d0ccbf 100644 --- a/solr/server/etc/agent-security.policy +++ b/solr/server/etc/agent-security.policy @@ -64,6 +64,22 @@ grant { permission java.io.FilePermission "/proc/-", "read"; permission java.io.FilePermission "/sys/-", "read"; + // --- RuntimePermission: exitVM / exec --- + // + // No exitVM grants are present in this default policy. This is intentional: + // - The Solr CLI tools (bin/solr foo bar) are not launched with -javaagent and therefore + // do not need an exitVM grant. + // - The running Solr backend (Jetty) is expected to shut down via graceful thread completion + // (ShutdownHook, server.stop()), not via System.exit(). Calling System.exit() from a plugin + // is the threat model, not the server itself. + // - In WARN mode (the default) any unapproved System.exit() call is logged but permitted. + // In ENFORCE mode it is blocked. Operators who need to allow a specific JAR or class to call + // System.exit() should add a codeBase-scoped RuntimePermission "exitVM" grant in + // agent-security-extra.policy. + // + // Similarly, no exec grants are present because the Solr server does not spawn child processes + // in its normal operation. Plugin code that needs process spawning must be explicitly granted. + // --- Outbound network access --- // Loopback addresses: unconditionally permitted (inter-thread, localhost HTTP, embedded ZK) From 2a923159a6dc4f98809bf0e7a6688a942b4fa12e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 13:46:57 +0200 Subject: [PATCH 51/65] Improve agents instruction around code comments, which tend to be too verbose. --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index f8325e396b24..46843a339e2d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,7 +64,7 @@ While README.md and CONTRIBUTING.md are mainly written for humans, this file is - For major or breaking changes, add a prominent note in reference guide major-changes-in-solr-X.adoc - Always consider whether a reference-guide page needs updating due to the new/changed features. Target audience is end user - For changes to build system and other developer-focused changes, consider updating or adding docs in dev-docs/ folder -- Keep all documentation including javadoc concise +- Keep all documentation including javadoc concise and not overly verbose, do not add a comment if it states the obvious. - New classes should have some javadocs - Changes should not have code comments communicating the change, which are instead great comments to leave for code review / commentary From 5122eab9aadad41876846a5083a0287bc6b731fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 13:53:19 +0200 Subject: [PATCH 52/65] Fix PermittedPath.permits: use Set membership instead of substring match actions.contains(action) does substring matching, so a malformed policy string like "readwrite" (missing comma) would incorrectly match both "read" and "write". Split the stored actions string into a Set at construction time and use Set.contains() for exact token matching. --- .../apache/solr/security/agent/PermittedPath.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java index 3803b4b059f7..041bebf2c163 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedPath.java @@ -17,7 +17,10 @@ package org.apache.solr.security.agent; import java.nio.file.Path; +import java.util.Arrays; import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; /** * A single file-system access rule from the security policy. @@ -29,13 +32,17 @@ public final class PermittedPath { private final String path; - private final String actions; + private final Set actionSet; + private final String actions; // retained for the actions() accessor private final boolean recursive; private final PolicySource source; PermittedPath(String path, String actions, boolean recursive, PolicySource source) { this.path = path; - this.actions = actions != null ? actions.toLowerCase(Locale.ROOT) : "read"; + String normalized = actions != null ? actions.toLowerCase(Locale.ROOT) : "read"; + this.actions = normalized; + this.actionSet = + Arrays.stream(normalized.split(",")).map(String::trim).collect(Collectors.toSet()); this.recursive = recursive; this.source = source; } @@ -69,6 +76,6 @@ public boolean permits(String resolvedPath, String action) { } else { pathMatch = resolvedPath.equals(path); } - return pathMatch && actions.contains(action.toLowerCase(Locale.ROOT)); + return pathMatch && actionSet.contains(action.toLowerCase(Locale.ROOT)); } } From 05db202e955f2a62e224f37b258e485e149f494c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 28 May 2026 13:54:54 +0200 Subject: [PATCH 53/65] Fix matchesEndpoint: strip brackets from IPv6 policy entries Policy files use "[::1]:8983" bracket notation for IPv6 but InetSocketAddress.getHostString() returns "::1" without brackets. lastIndexOf(':') found the colon after ']' correctly, but the extracted entryHost retained the brackets and never equaled the actual host string. Parse bracket-notation IPv6 entries separately: strip the '[' and ']' before passing to matchesHost so "::1" matches "[::1]:8983". Add two tests covering the bracket notation with an exact port and a port range. --- .../agent/SocketChannelInterceptor.java | 24 +++++++++++++------ .../agent/SocketChannelInterceptorTest.java | 18 ++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java index 02ba0336335a..0c3a0fa9d62b 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java @@ -186,16 +186,26 @@ static boolean isCallerFromCodeBase(Collection> chain, String codeBase) public static boolean matchesEndpoint(String hostPortEntry, String host, int port) { if ("*".equals(hostPortEntry)) return true; - int colonIdx = hostPortEntry.lastIndexOf(':'); - if (colonIdx < 0) { - return matchesHost(hostPortEntry, host); + String entryHost; + String entryPort; + if (hostPortEntry.startsWith("[")) { + // IPv6 bracket notation: "[::1]:8983" or "[::1]:1-65535" + int closeBracket = hostPortEntry.indexOf(']'); + if (closeBracket < 0) return false; + entryHost = hostPortEntry.substring(1, closeBracket); // strip brackets + int colonAfterBracket = hostPortEntry.indexOf(':', closeBracket + 1); + entryPort = colonAfterBracket >= 0 ? hostPortEntry.substring(colonAfterBracket + 1) : null; + } else { + int colonIdx = hostPortEntry.lastIndexOf(':'); + if (colonIdx < 0) { + return matchesHost(hostPortEntry, host); + } + entryHost = hostPortEntry.substring(0, colonIdx); + entryPort = hostPortEntry.substring(colonIdx + 1); } - String entryHost = hostPortEntry.substring(0, colonIdx); - String entryPort = hostPortEntry.substring(colonIdx + 1); - if (!matchesHost(entryHost, host)) return false; - return matchesPort(entryPort, port); + return entryPort == null || matchesPort(entryPort, port); } public static boolean matchesHost(String entryHost, String actualHost) { diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java index 9a87cb2c54f3..4e229b08ce71 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/SocketChannelInterceptorTest.java @@ -138,4 +138,22 @@ public void testBroadWildcardPermitsAll() { AgentPolicy policy = policyWithEndpoint("*"); assertTrue(SocketChannelInterceptor.isEndpointPermitted(policy, "anything.com", 443)); } + + @Test + public void testIPv6BracketNotationMatches() { + // Policy entries for IPv6 use bracket notation "[::1]:port"; the actual host from + // InetSocketAddress.getHostString() is without brackets. Verify stripping works. + AgentPolicy policy = policyWithEndpoint("[::1]:8983"); + assertTrue(SocketChannelInterceptor.isEndpointPermitted(policy, "::1", 8983)); + assertFalse(SocketChannelInterceptor.isEndpointPermitted(policy, "::1", 8984)); + assertFalse(SocketChannelInterceptor.isEndpointPermitted(policy, "::2", 8983)); + } + + @Test + public void testIPv6BracketWithPortRange() { + AgentPolicy policy = policyWithEndpoint("[::1]:1-65535"); + assertTrue(SocketChannelInterceptor.isEndpointPermitted(policy, "::1", 8983)); + assertTrue(SocketChannelInterceptor.isEndpointPermitted(policy, "::1", 1)); + assertFalse(SocketChannelInterceptor.isEndpointPermitted(policy, "::2", 8983)); + } } From a1bab4f65f25a50cb0f50ee574cb72e104b98350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 29 May 2026 23:40:53 +0200 Subject: [PATCH 54/65] Fix FileInterceptor: catch SecurityException and normalize policy paths Two bugs that cause Solr to fail to start when the Old Java SecurityManager is active (SOLR_SECURITY_MANAGER_ENABLED=true, the default for Java < 24): 1. resolveRealPath() only caught IOException, not SecurityException. On JDK 21 and earlier, Path.toRealPath() calls sm.checkRead() on the original path internally; if that check fails (e.g. when the Old SM policy does not cover the accessed path), a SecurityException is thrown. Because the advice signature declares 'throws Exception' but the catch clause was IOException-only, the SecurityException propagated through the ByteBuddy advice and aborted the intercepted file operation, preventing Solr from starting. Fix: catch IOException | SecurityException and fall back to toAbsolutePath().normalize() in both cases. 2. Policy paths from agent-security.policy were stored verbatim after variable substitution without resolving symlinks. At runtime, resolveRealPath() returns the real (symlink-resolved) form, which would not match a symlinked policy path. Fix: normalize each FilePermission base path via toRealPath() (same fallback logic as the runtime check) when building the policy, so both sides of the comparison use the canonical form. Also updates the stale comment in agent-security.policy that said "FileInterceptor uses toAbsolutePath(), not toRealPath()". --- .../apache/solr/security/agent/FileInterceptor.java | 5 +++-- .../apache/solr/security/agent/PolicyLoader.java | 13 +++++++++++++ solr/server/etc/agent-security.policy | 5 +++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java index ee7d727ded6a..79b1fc3930c1 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java @@ -57,8 +57,9 @@ static String resolveRealPath(Path path) { IN_SYMLINK_RESOLVE.set(true); try { return path.toRealPath().toString(); - } catch (IOException e) { - // Path does not exist yet — fall back to normalized path + } catch (IOException | SecurityException e) { + // Path does not exist yet, or the Old Java SecurityManager blocked checkRead() on the + // resolved real path — fall back to the normalized (non-symlink-resolved) path. return path.toAbsolutePath().normalize().toString(); } finally { IN_SYMLINK_RESOLVE.set(false); diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java index 5320c7b7cda2..6b6753c3268c 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java @@ -208,6 +208,19 @@ private AgentPolicy buildPolicy(List grants) { for (RawFilePermission fp : block.filePaths) { boolean recursive = fp.target.endsWith("/-") || fp.target.endsWith("\\-"); String basePath = recursive ? fp.target.substring(0, fp.target.length() - 2) : fp.target; + // Normalize the policy path using the same resolution strategy as FileInterceptor so that + // comparisons are always apples-to-apples. toRealPath() resolves symlinks; fall back to + // toAbsolutePath().normalize() when the path does not exist yet or the Old Java + // SecurityManager blocks the read check on the resolved path. + try { + basePath = Path.of(basePath).toRealPath().toString(); + } catch (IOException | SecurityException e) { + try { + basePath = Path.of(basePath).toAbsolutePath().normalize().toString(); + } catch (Exception ignored) { + // keep the variable-substituted string as-is + } + } paths.add(new PermittedPath(basePath, fp.actions, recursive, fp.source)); } for (RawSocketPermission sp : block.socketPerms) { diff --git a/solr/server/etc/agent-security.policy b/solr/server/etc/agent-security.policy index 4fa7b3d0ccbf..28f28386c102 100644 --- a/solr/server/etc/agent-security.policy +++ b/solr/server/etc/agent-security.policy @@ -38,8 +38,9 @@ grant { // Solr installation directory: read-only (JARs, modules, Jetty config, etc.) // Both the real path and the symlinked path are permitted so that installations where - // /opt/solr -> /opt/solr-X.Y.Z/ work correctly (FileInterceptor uses toAbsolutePath(), - // not toRealPath(), so accesses via the symlink path must be explicitly allowed). + // /opt/solr -> /opt/solr-X.Y.Z/ work correctly regardless of which form is presented to + // FileInterceptor. When the Old Java SecurityManager is active, toRealPath() may fall back + // to toAbsolutePath().normalize(), producing the symlinked form; both entries are needed. permission java.io.FilePermission "${solr.install.dir}/-", "read"; permission java.io.FilePermission "${solr.install.symDir}/-", "read"; From 3533c70d4815cb9ff03b5567623e88c745afe2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 29 May 2026 23:57:19 +0200 Subject: [PATCH 55/65] Fix testDocker: use doNotTrackState to avoid AccessDeniedException Gradle's incremental-build state tracking reads the @OutputDirectory before running the task. Docker containers run as uid 8983 (the solr user); any files they create (e.g. logs/) are unreadable by the Gradle runner process, causing an AccessDeniedException before the test even starts. Adding doNotTrackState() bypasses the output-directory inspection so the task always runs regardless of prior leftover artifacts. --- solr/docker/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/solr/docker/build.gradle b/solr/docker/build.gradle index e29b892594b2..dc6163c6b742 100644 --- a/solr/docker/build.gradle +++ b/solr/docker/build.gradle @@ -253,6 +253,11 @@ task testDocker(type: TestDockerImageTask, dependsOn: tasks.dockerBuild) { group = 'Docker' description = 'Test Solr docker image built from the local Dockerfile' + // Docker containers run as uid 8983 (solr user) and create files that the Gradle runner + // (a different uid) cannot read. Disabling state-tracking avoids an AccessDeniedException + // when Gradle tries to inspect the output directory for incremental-build purposes. + doNotTrackState("Docker containers produce uid-8983 owned files unreadable by the Gradle process") + idFile = tasks.dockerBuild.outputs.files.singleFile outputDir = file("$buildDir/test-results") From 7d51358f45ff950634917d67781d88580646e2fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sat, 30 May 2026 01:53:54 +0200 Subject: [PATCH 56/65] Fix FileInterceptor: make resolveRealPath and enforceFileAccess public ByteBuddy inlines @Advice.OnMethodEnter bytecode into JDK methods (e.g. java.nio.file.Files). Any static helper called from inlined advice must be public; package-private access causes IllegalAccessError at runtime because the inlined call-site executes in the context of the JDK class, not FileInterceptor. This was causing Solr startup failures on Linux with JDK 21 when the Old SecurityManager policy loaded lazily via Files.newByteChannel. --- .../java/org/apache/solr/security/agent/FileInterceptor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java index 79b1fc3930c1..f6ebe6442d53 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/FileInterceptor.java @@ -50,7 +50,7 @@ public FileInterceptor() {} * (b) {@code toRealPath()} itself triggers a re-entrant interception (detected via {@link * #IN_SYMLINK_RESOLVE}). */ - static String resolveRealPath(Path path) { + public static String resolveRealPath(Path path) { if (Boolean.TRUE.equals(IN_SYMLINK_RESOLVE.get())) { return path.toAbsolutePath().normalize().toString(); } @@ -249,7 +249,7 @@ public static void intercept(@Advice.AllArguments Object[] args, @Advice.Origin * Increments the file violation counter and logs; throws {@link SecurityException} in enforce * mode. Used by both the {@link #intercept} advice and the test-side check helpers. */ - static void enforceFileAccess( + public static void enforceFileAccess( AgentPolicy policy, String resolvedPath, String action, From a5cad1a8f64f774851648dcd835c6f892ecae4c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sat, 30 May 2026 01:53:58 +0200 Subject: [PATCH 57/65] Trim comments and Javadoc in agent-sm: remove obvious and redundant text --- .../solr/security/agent/AgentPolicy.java | 81 ++++--------------- .../solr/security/agent/ApprovedCallSite.java | 41 ++-------- .../solr/security/agent/PolicyLoader.java | 63 +++------------ .../agent/ProcessExecInterceptor.java | 41 ++-------- .../agent/SocketChannelInterceptor.java | 31 +------ .../security/agent/SolrAgentEntryPoint.java | 57 +++---------- .../agent/StackCallerClassChainExtractor.java | 4 - .../agent/ViolationMetricsReporter.java | 9 --- .../solr/security/agent/package-info.java | 61 ++++---------- 9 files changed, 72 insertions(+), 316 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java index 10e15e3b6397..cd74e5f4166a 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java @@ -22,18 +22,11 @@ import java.util.Set; /** - * Immutable singleton that holds the active security policy for the Solr JVM. + * Immutable singleton holding the active security policy for the Solr JVM. * - *

    The policy is loaded once at JVM startup by {@link PolicyLoader} and must not be modified - * afterwards. Any attempt to replace the singleton after it has been set throws a {@link - * SecurityException}. - * - *

    The singleton is stored as a plain {@code static volatile} field so that it is visible to all - * classloaders, including bootstrap-injected agent classes. The enforcement mode is read directly - * from the system property {@code solr.security.agent.mode} (set by the startup script from the - * environment variable {@code SOLR_SECURITY_AGENT_MODE}). {@code EnvUtils} from {@code solr:core} - * is intentionally not used here because the agent JAR has no compile-time dependency on Solr - * application code. + *

    Loaded once at startup by {@link PolicyLoader}; replacing it throws {@link SecurityException}. + * Stored as {@code static volatile} so it is visible to the bootstrap classloader used by the agent + * JAR. */ public final class AgentPolicy { @@ -74,11 +67,8 @@ public enum EnforcementMode { } /** - * Constructs the policy with explicit trusted filesystem schemes and trusted hosts. - * - * @param trustedFileSystems filesystem URI schemes exempt from path checks (e.g. {@code "jrt"}, - * {@code "memory"} used in tests) - * @param trustedHosts host strings exempt from network checks (e.g. loopback addresses) + * Constructs the policy with explicit trusted filesystem schemes (e.g. {@code "jrt"}, {@code + * "memory"}) and trusted hosts (e.g. loopback addresses). */ AgentPolicy( List permittedPaths, @@ -131,24 +121,12 @@ public static AgentPolicy getInstance() { return p; } - /** - * Returns {@code true} if the singleton has already been initialized. Used by the agent entry - * point to detect double-loading. - */ + /** Returns {@code true} if the singleton has been initialized. */ public static boolean isInitialized() { return instance != null; } - /** - * Resets the singleton to {@code null} so that tests can re-initialize it between test methods. - * - *

    This method is package-private and intended exclusively for unit tests in the {@code - * org.apache.solr.security.agent} package. Production code must never call this method. - * - *

    The write is not synchronized: {@code instance} is {@code volatile}, so the assignment is - * immediately visible to all threads. Unlike {@link #initialize}, there is no invariant to - * protect here — tests call this only from a single thread during teardown. - */ + /** Resets the singleton for tests; must not be called from production code. */ static void resetForTesting() { instance = null; } @@ -179,10 +157,7 @@ public List approvedExecCallers() { return approvedExecCallers; } - /** - * Current enforcement mode. {@link EnforcementMode#WARN} allows violations; {@link - * EnforcementMode#ENFORCE} blocks them. - */ + /** Current enforcement mode. */ public EnforcementMode enforcementMode() { return enforcementMode; } @@ -195,10 +170,7 @@ public Set trustedFileSystems() { return trustedFileSystems; } - /** - * Host strings exempt from outbound network checks (e.g. {@code "localhost"}, {@code - * "127.0.0.1"}). Populated by {@link SolrAgentEntryPoint} at startup. - */ + /** Host strings exempt from outbound network checks (e.g. {@code "localhost"}). */ public Set trustedHosts() { return trustedHosts; } @@ -207,10 +179,7 @@ public Set trustedHosts() { // Policy checks (convenience helpers called by interceptors) // --------------------------------------------------------------------------- - /** - * Returns {@code true} if at least one {@link PermittedPath} in this policy permits the given - * action on the given resolved (real) path. - */ + /** Returns {@code true} if the policy permits {@code action} on the resolved path. */ public boolean isPathPermitted(String resolvedPath, String action) { for (PermittedPath p : permittedPaths) { if (p.permits(resolvedPath, action)) return true; @@ -218,10 +187,7 @@ public boolean isPathPermitted(String resolvedPath, String action) { return false; } - /** - * Returns {@code true} if at least one {@link ApprovedCallSite} with {@link - * ApprovedCallSite.Operation#EXIT} matches the given class name. - */ + /** Returns {@code true} if {@code className} matches an approved exit call-site. */ public boolean isExitApproved(String className) { for (ApprovedCallSite cs : approvedExitCallers) { // codeBase entries require a Class object; skip them here @@ -232,10 +198,7 @@ public boolean isExitApproved(String className) { return false; } - /** - * Returns {@code true} if at least one {@link ApprovedCallSite} with {@link - * ApprovedCallSite.Operation#EXEC} matches the given class name. - */ + /** Returns {@code true} if {@code className} matches an approved exec call-site. */ public boolean isExecApproved(String className) { for (ApprovedCallSite cs : approvedExecCallers) { // codeBase entries require a Class object; skip them here @@ -247,14 +210,8 @@ public boolean isExecApproved(String className) { } /** - * Returns {@code true} if any class in the call chain is approved to call {@code System.exit()} - * or {@code Runtime.halt()}. Any approved class anywhere in the chain grants permission. - * - *

    Class names are matched by {@link ApprovedCallSite#matches}: {@code "*"} matches any class; - * a pattern ending in {@code ".*"} matches the named package and all its sub-packages; anything - * else is an exact class-name match. - * - * @param chain the full set of non-hidden classes in the call stack + * Returns {@code true} if any class in {@code chain} is approved to call {@code System.exit()} + * or {@code Runtime.halt()}. Any match anywhere in the chain is sufficient. */ public boolean isChainThatCanExit(Collection> chain) { for (Class cls : chain) { @@ -269,12 +226,8 @@ public boolean isChainThatCanExit(Collection> chain) { } /** - * Returns {@code true} if any class in the call chain is approved to spawn child processes via - * {@code ProcessBuilder.start()} or {@code Runtime.exec()}. Any approved class anywhere in the - * chain grants permission, mirroring the exit-caller semantics in {@link - * #isChainThatCanExit(Collection)}. - * - * @param chain the full set of non-hidden classes in the call stack + * Returns {@code true} if any class in {@code chain} is approved to spawn child processes. + * Same semantics as {@link #isChainThatCanExit(Collection)}. */ public boolean isChainThatCanExec(Collection> chain) { for (Class cls : chain) { diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java index d7fee41b8406..7853e8a7fe43 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java @@ -19,26 +19,12 @@ import java.util.List; /** - * A call-site pattern that is approved to perform a restricted operation such as calling {@code - * System.exit()} or spawning a child process via {@code ProcessBuilder}. + * A policy entry approving a class (or code source) to perform a restricted operation. * - *

    Approved call sites are loaded from the security policy at startup and are immutable. Each - * entry matches callers either by class-name pattern or by code-source location (codeBase). - * - *

    Class-name matching (when {@link #codeBase()} is {@code null}): uses the same syntax as - * {@link AgentPolicy#isChainThatCanExit}: {@code "*"} matches any class; a pattern ending in {@code - * ".*"} (e.g. {@code org.apache.solr.cli.*}) matches that package and all sub-packages; anything - * else is an exact fully-qualified class name. - * - *

    codeBase matching (when {@link #codeBase()} is non-{@code null}): the calling class - * must have been loaded from a JAR or directory matching the codeBase URL, using the same syntax as - * JDK policy files ({@code file:/path/-} for recursive, {@code file:/path/to.jar} for exact). - * - *

    Default approved EXIT callers: none (empty list in the bundled production policy). Operators - * can add entries via {@code agent-security-extra.policy} when specific code needs to call {@code - * System.exit()} — for example, a custom lifecycle plugin. - * - *

    Default approved EXEC callers: none (empty list in production policy). + *

    Class-name matching ({@link #codeBase()} is {@code null}): {@code "*"} matches any class; a + * pattern ending in {@code ".*"} matches that package and sub-packages; otherwise exact match. + * codeBase matching: the calling class must have been loaded from a location matching the JDK + * policy {@code codeBase} URL ({@code file:/path/-} recursive, {@code file:/path/to.jar} exact). */ public final class ApprovedCallSite { @@ -53,15 +39,10 @@ public enum Operation { private final Operation operation; private final PolicySource source; - /** Constructs a class-name–based approval (no codeBase constraint). */ ApprovedCallSite(String classNamePattern, Operation operation, PolicySource source) { this(classNamePattern, null, operation, source); } - /** - * Constructs an approval that is either class-name–based ({@code codeBase == null}) or - * code-source–based ({@code classNamePattern == null}). - */ ApprovedCallSite( String classNamePattern, String codeBase, Operation operation, PolicySource source) { this.classNamePattern = classNamePattern; @@ -96,11 +77,7 @@ public PolicySource source() { return source; } - /** - * Returns {@code true} if the given fully-qualified class name matches this entry's class-name - * pattern. Always returns {@code false} when this entry uses codeBase matching — use {@link - * #matchesCodeBase(Class)} instead. - */ + /** Returns {@code true} if {@code className} matches the pattern; {@code false} for codeBase entries. */ public boolean matches(String className) { if (codeBase != null) return false; // codeBase entries must be checked via matchesCodeBase if ("*".equals(classNamePattern)) return true; @@ -111,11 +88,7 @@ public boolean matches(String className) { return classNamePattern.equals(className); } - /** - * Returns {@code true} if this entry has a codeBase constraint and the given class was loaded - * from a code source matching that codeBase. Always returns {@code false} when this entry uses - * class-name matching. - */ + /** Returns {@code true} if {@code cls} was loaded from this entry's codeBase; {@code false} for class-name entries. */ public boolean matchesCodeBase(Class cls) { if (codeBase == null) return false; return SocketChannelInterceptor.isCallerFromCodeBase(List.of(cls), codeBase); diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java index 6b6753c3268c..6199ae059560 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java @@ -26,26 +26,10 @@ import java.util.Set; /** - * Reads and parses JDK-style {@code .policy} files, performing Solr-specific variable substitution, - * and produces a {@link AgentPolicy} ready for enforcement. - * - *

    Variable substitution

    - * - * {@code ${property}} placeholders in permission targets and {@code codeBase} URLs are expanded - * per-token by {@link PolicyPropertyExpander} using system properties. Any unresolved placeholder - * causes startup to fail immediately (fail-fast). The only built-in default is {@code - * ${solr.zk.port}}, which falls back to {@code solr.port + 1000} when not explicitly set. - * - *

    Two-file merge

    - * - * Two files are loaded: the mandatory default policy and an optional operator extension file. The - * extra-policy file path is resolved from system property {@code solr.security.agent.extra.policy}, - * falling back to {@code ${server.dir}/etc/agent-security-extra.policy}. An absent extra-policy - * file is silently skipped. Each entry carries a {@link PermittedPath#source() source} tag of - * either {@link PolicySource#DEFAULT} or {@link PolicySource#OPERATOR}. - * - *

    A missing or unparseable default policy causes an {@link IllegalStateException} at - * startup. + * Parses JDK-style {@code .policy} files with Solr variable substitution and produces an {@link + * AgentPolicy}. Loads the mandatory default policy and an optional operator extension file (path + * from {@code solr.security.agent.extra.policy} or {@code ${server.dir}/etc/agent-security-extra.policy}). + * Unresolved {@code ${property}} placeholders fail fast at startup. */ public class PolicyLoader { @@ -82,7 +66,6 @@ public AgentPolicy load(Path defaultPolicyPath) { + ". The default policy must define at least one grant."); } - // Resolve extra-policy path: system property → fallback to ${server.dir}/etc/... Path extraPolicyPath = resolveExtraPolicyPath(); if (extraPolicyPath != null && Files.exists(extraPolicyPath)) { String extraContent; @@ -104,12 +87,7 @@ public AgentPolicy load(Path defaultPolicyPath) { return buildPolicy(grants); } - /** - * Resolves the extra-policy file path from system property {@code - * solr.security.agent.extra.policy}, falling back to {@code - * ${server.dir}/etc/agent-security-extra.policy}. Returns {@code null} if no fallback is - * available. - */ + /** Returns the extra-policy path, or {@code null} if neither sysprop nor {@code jetty.home} is set. */ static Path resolveExtraPolicyPath() { String explicitPath = PolicyPropertyExpander.getPropertyOrEnv("solr.security.agent.extra.policy"); @@ -123,29 +101,15 @@ static Path resolveExtraPolicyPath() { return null; } - /** - * Parses a policy file and appends the resulting {@link GrantBlock} entries — tagged with the - * given {@code source} — to {@code out}. Variable substitution is performed per-token by {@link - * PolicyPropertyExpander}; any unresolved {@code ${variable}} causes an {@link - * IllegalStateException}. - */ static void parsePolicy(String content, PolicySource source, List out) { parsePolicyBlocks(content, source, out); } /** - * Parses grant blocks from the given (already variable-substituted) policy text. Only the - * permission types used by the Solr agent are recognised: - * - *

      - *
    • {@code java.io.FilePermission} → {@link PermittedPath} - *
    • {@code java.net.SocketPermission} → {@link PermittedEndpoint} - *
    • {@code java.lang.RuntimePermission "exitVM"} → {@link ApprovedCallSite} EXIT - *
    • {@code java.lang.RuntimePermission "exec"} → {@link ApprovedCallSite} EXEC - *
    - * - *

    Parsing uses {@link PolicyFileParser} (backed by {@link java.io.StreamTokenizer}) which - * natively handles {@code //} and {@code /* *\/} comments and quoted strings — no regex. + * Parses grant blocks. Recognized permission types: + * {@code java.io.FilePermission} → {@link PermittedPath}, + * {@code java.net.SocketPermission} → {@link PermittedEndpoint}, + * {@code java.lang.RuntimePermission "exitVM"|"exec"} → {@link ApprovedCallSite}. */ static void parsePolicyBlocks(String text, PolicySource source, List out) { List grantEntries; @@ -208,10 +172,7 @@ private AgentPolicy buildPolicy(List grants) { for (RawFilePermission fp : block.filePaths) { boolean recursive = fp.target.endsWith("/-") || fp.target.endsWith("\\-"); String basePath = recursive ? fp.target.substring(0, fp.target.length() - 2) : fp.target; - // Normalize the policy path using the same resolution strategy as FileInterceptor so that - // comparisons are always apples-to-apples. toRealPath() resolves symlinks; fall back to - // toAbsolutePath().normalize() when the path does not exist yet or the Old Java - // SecurityManager blocks the read check on the resolved path. + // Resolve symlinks so policy paths compare apples-to-apples with FileInterceptor. try { basePath = Path.of(basePath).toRealPath().toString(); } catch (IOException | SecurityException e) { @@ -256,8 +217,8 @@ private AgentPolicy buildPolicy(List grants) { // 0.0.0.0 is the unspecified bind address, not a loopback — intentionally excluded. Set trustedHosts = Set.of("localhost", "127.0.0.1", "0:0:0:0:0:0:0:1", "::1"); - // jar/zip/jrt are internal JVM class-loading file systems. Intercepting them causes a - // class-initialization deadlock: violation logger → SLF4J → Log4j2 init → JAR read → repeat. + // jar/zip/jrt: JVM class-loading file systems. Intercepting them causes a + // classloader deadlock (violation logger → SLF4J → Log4j2 init → JAR read → repeat). Set trustedFileSystems = Set.of("jar", "zip", "jrt"); return new AgentPolicy( paths, endpoints, exitCallers, execCallers, mode, trustedFileSystems, trustedHosts); diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java index a7443ec0f78e..fb34aca673c4 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java @@ -22,39 +22,15 @@ import net.bytebuddy.asm.Advice; /** - * ByteBuddy {@link Advice} interceptor for child process spawning. - * - *

    Intercepts {@code ProcessBuilder.start()} and {@code Runtime.exec()} to enforce the {@link - * AgentPolicy#approvedExecCallers()} list. By default, no call sites are approved in the production - * policy (the list is empty), so all process-spawning attempts will be flagged unless an operator - * explicitly adds an entry to {@code agent-security-extra.policy}. - * - *

    Permission is granted if any class in the full call chain matches an approved entry — - * the same semantics as {@link SystemExitInterceptor}. This means that if an approved class (e.g. - * {@code SolrCLI}) calls a helper that then calls {@code ProcessBuilder.start()}, the helper call - * is still permitted. - * - *

    Known legitimate process-spawning call sites in Solr (kept out of the default production - * policy because they use {@code ProcessHandle}, not {@code ProcessBuilder}): - * - *

      - *
    • {@code org.apache.solr.cli.SolrProcessManager} — JVM discovery - *
    + * ByteBuddy {@link Advice} interceptor for {@code ProcessBuilder.start()} and {@code + * Runtime.exec()}. Permission is granted if any class in the full call chain matches an approved + * entry — same semantics as {@link SystemExitInterceptor}. */ public final class ProcessExecInterceptor { private ProcessExecInterceptor() {} - /** - * Single entry point for both {@code ProcessBuilder.start()} and {@code Runtime.exec()}. - * - *

    ByteBuddy requires exactly one {@code @OnMethodEnter} method per advice class. This method - * is registered on both {@code ProcessBuilder} and {@code Runtime} via separate {@code - * AgentBuilder} transform chains in {@link SolrAgentEntryPoint}. - * - * @param args all arguments of the intercepted method - * @param method the intercepted method (used to identify the call site in the violation log) - */ + /** Shared entry point for {@code ProcessBuilder.start()} and {@code Runtime.exec()}. */ @Advice.OnMethodEnter(suppress = IOException.class) public static void onExec(@Advice.AllArguments Object[] args, @Advice.Origin Method method) { checkExec(deriveTarget(method.getName(), args)); @@ -75,14 +51,7 @@ public static String deriveTarget(String methodName, Object[] args) { // Core check logic // --------------------------------------------------------------------------- - /** - * Checks whether any class in the current call chain is in the approved exec-caller list. Uses - * the full chain walk (same semantics as {@link SystemExitInterceptor}) so that an approved class - * anywhere in the stack grants permission. Delegates to {@link SecurityViolationLogger} on - * violation. - * - * @param target a human-readable description of the intercepted call for the violation log - */ + /** Checks the call chain against the approved exec-caller list; logs and throws in enforce mode. */ public static void checkExec(String target) { if (!AgentPolicy.isInitialized()) return; diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java index 0c3a0fa9d62b..65790aed4d58 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java @@ -31,15 +31,8 @@ */ public class SocketChannelInterceptor { - /** SocketChannelInterceptor */ public SocketChannelInterceptor() {} - /** - * Intercepts outbound socket connections. - * - * @param args arguments of the intercepted {@code connect} method - * @throws Exception if the connection is denied in enforce mode - */ @Advice.OnMethodEnter public static void intercept(@Advice.AllArguments Object[] args) throws Exception { if (!AgentPolicy.isInitialized()) return; @@ -74,12 +67,7 @@ public static void intercept(@Advice.AllArguments Object[] args) throws Exceptio // Shared enforcement helper // --------------------------------------------------------------------------- - /** - * Enforces the network policy for {@code host:port}. Increments the network violation counter, - * logs, and throws {@link SecurityException} in enforce mode if no permitted endpoint matches. - * Used by both the {@link #intercept} advice and the test-side helper in {@code - * InterceptorTestHelper}. - */ + /** Checks the policy for {@code host:port}; logs and throws {@link SecurityException} in enforce mode. */ static void enforceNetworkAccess(AgentPolicy policy, String host, int port, String caller) { if (!isEndpointPermitted(policy, host, port)) { String target = host + ":" + port; @@ -101,20 +89,9 @@ static void enforceNetworkAccess(AgentPolicy policy, String host, int port, Stri // --------------------------------------------------------------------------- /** - * Returns {@code true} if at least one permitted endpoint entry in the policy covers the given - * host and port. Matching rules: - * - *

      - *
    • Entry {@code *:port} — matches any host on that exact port - *
    • Entry {@code host:port} — matches exact host and port - *
    • Entry {@code host:low-high} — matches the host with a port in the inclusive range - *
    • Entry {@code *} (no colon) — matches everything (broad wildcard) - *
    - * - *

    Entries with a {@code codeBase} restriction are evaluated against the current call chain via - * {@link StackWalker}: the entry permits the connection only if at least one class in the chain - * was loaded from a code source under that codeBase path. The stack walk is performed lazily — - * only when a codeBase-restricted entry whose endpoint pattern matches is encountered. + * Returns {@code true} if the policy permits an outbound connection to {@code host:port}. + * Matching rules: {@code *:port} — any host on that port; {@code host:low-high} — port range; + * {@code *} — everything. codeBase-restricted entries are checked lazily via {@link StackWalker}. */ public static boolean isEndpointPermitted(AgentPolicy policy, String host, int port) { Collection> chain = null; // lazily populated diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java index 58ca26de86a6..4cae280058ad 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java @@ -29,38 +29,16 @@ import net.bytebuddy.matcher.ElementMatchers; /** - * Java agent entry point for the Solr security agent. - * - *

    The JVM invokes {@link #premain(String, Instrumentation)} before the application main method - * when the agent JAR is listed on the command line as {@code -javaagent:solr-agent-sm-*.jar}. The - * {@link #agentmain(String, Instrumentation)} method supports attach-based loading (not used by - * Solr's startup scripts but retained for tooling compatibility). - * - *

    Startup sequence

    - * - *
      - *
    1. Locate and parse {@code agent-security.policy} (and the optional {@code - * agent-security-extra.policy}) via {@link PolicyLoader}. - *
    2. Initialize the {@link AgentPolicy} singleton. - *
    3. Register all four ByteBuddy interceptors with the JVM instrumentation API. - *
    4. If policy loading fails and enforcement mode is {@code ENFORCE}, halt the JVM; in {@code - * WARN} mode, log the error and continue without protection. - *
    - * - *

    Bootstrap classloader

    - * - * The agent JAR is placed on {@code Boot-Class-Path} so all interceptor and policy classes are - * visible to the bootstrap classloader. SLF4J is intentionally absent from the fat JAR to avoid - * poisoning the SLF4J binding before Log4j2 is loaded. + * Java agent entry point. Invoked by the JVM before the application main class when the agent JAR + * is specified via {@code -javaagent:}. Loads the policy, initializes {@link AgentPolicy}, and + * installs ByteBuddy interceptors. The agent JAR is on {@code Boot-Class-Path} so interceptor + * classes are visible to the bootstrap classloader; SLF4J is excluded from the fat JAR to avoid + * poisoning the binding before Log4j2 initializes. */ public final class SolrAgentEntryPoint { private SolrAgentEntryPoint() {} - /** - * Called by the JVM before the application main class is loaded, when the agent JAR is specified - * via {@code -javaagent:}. - */ public static void premain(String agentArgs, Instrumentation inst) { bootAgent(inst); } @@ -79,7 +57,6 @@ public static void agentmain(String agentArgs, Instrumentation inst) { "System.err is the only safe output during premain; System.exit(1) is required to halt " + "the JVM on fatal policy-load failure in enforce mode.") private static void bootAgent(Instrumentation inst) { - // Locate the default policy file next to the agent JAR. Path defaultPolicyPath = resolveDefaultPolicyPath(); AgentPolicy policy = null; @@ -135,13 +112,8 @@ private static void bootAgent(Instrumentation inst) { }; private static void installInterceptors(Instrumentation inst) { - // AgentBuilder configuration for JDK class instrumentation: - // - Implementation.Context.Disabled: required for REDEFINE so ByteBuddy does not try to - // add auxiliary types (forbidden when redefining already-loaded JDK classes). - // - InitializationStrategy.NoOp: skip static initializer injection (same reason). - // - RedefinitionStrategy.REDEFINITION: redefine already-loaded bootstrap classes (Files, - // etc.) - // in-place rather than scheduling a retransformation pass. + // Context.Disabled + NoOp: required for REDEFINE — ByteBuddy must not add auxiliary types or + // static initializers when redefining already-loaded JDK classes. final ByteBuddy byteBuddy = new ByteBuddy().with(Implementation.Context.Disabled.Factory.INSTANCE); @@ -149,10 +121,8 @@ private static void installInterceptors(Instrumentation inst) { .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE) .with(AgentBuilder.RedefinitionStrategy.REDEFINITION) .with(AgentBuilder.TypeStrategy.Default.REDEFINE) - // Override the default ignore filter so we can instrument bootstrap-loaded JDK classes - // in named modules (java.lang, java.nio, etc.). Without this, ByteBuddy 1.18.x silently - // skips those classes due to module-visibility checks in its default ignore rules. - // Also exclude ByteBuddy's own classes to prevent circular instrumentation at startup. + // Custom ignore filter: allows bootstrap JDK classes (java.lang, java.nio, …) to be + // instrumented; excludes ByteBuddy's own classes to prevent circular instrumentation. .ignore(ElementMatchers.isSynthetic().or(ElementMatchers.nameStartsWith("net.bytebuddy."))) // Intercept FileSystemProvider subclasses, java.nio.file.Files, and FileChannel subtypes. .type( @@ -183,8 +153,7 @@ private static void installInterceptors(Instrumentation inst) { (builder, type, classLoader, module, domain) -> builder.visit( Advice.to(SystemExitInterceptor.class).on(ElementMatchers.named("exit")))) - // Intercept Runtime.halt(int) and Runtime.exec(...) in a single chain to avoid - // applying two separate redefinitions to the same class. + // halt and exec together: avoids two separate redefinitions of Runtime .type(ElementMatchers.is(Runtime.class)) .transform( (builder, type, classLoader, module, domain) -> @@ -202,11 +171,7 @@ private static void installInterceptors(Instrumentation inst) { .installOn(inst); } - /** - * Resolves the default policy file path. Looks for {@code server/etc/agent-security.policy} - * relative to the Solr installation root ({@code solr.install.dir}), then falls back to {@code - * jetty.home/../etc/agent-security.policy}. - */ + /** Resolves {@code server/etc/agent-security.policy} from {@code solr.install.dir} or {@code jetty.home}. */ private static Path resolveDefaultPolicyPath() { String installDir = System.getProperty("solr.install.dir"); if (installDir != null && !installDir.isBlank()) { diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/StackCallerClassChainExtractor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/StackCallerClassChainExtractor.java index f72c0f607c5b..db9258bb592f 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/StackCallerClassChainExtractor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/StackCallerClassChainExtractor.java @@ -26,16 +26,12 @@ /** * Extracts the set of all non-hidden declaring classes from the current call stack. * - *

    Used by {@link SystemExitInterceptor} and {@link RuntimeHaltInterceptor} to obtain the full - * caller chain for exit-permission checks. - * *

    This file was derived from the OpenSearch project and modified. See {@code NOTICE.txt} for * attribution. */ public final class StackCallerClassChainExtractor implements Function, Collection>> { - /** Single instance of stateless class. */ public static final StackCallerClassChainExtractor INSTANCE = new StackCallerClassChainExtractor(); diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java index 5341702ca097..4a3b3728f755 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ViolationMetricsReporter.java @@ -26,7 +26,6 @@ */ public final class ViolationMetricsReporter { - // Per-type counters — incremented atomically from interceptor hot paths. private static final LongAdder FILE_COUNTER = new LongAdder(); private static final LongAdder NETWORK_COUNTER = new LongAdder(); private static final LongAdder EXIT_COUNTER = new LongAdder(); @@ -34,42 +33,34 @@ public final class ViolationMetricsReporter { private ViolationMetricsReporter() {} - /** Increments the file-access violation counter. */ public static void incrementFile() { FILE_COUNTER.increment(); } - /** Increments the network-connection violation counter. */ public static void incrementNetwork() { NETWORK_COUNTER.increment(); } - /** Increments the System.exit() / Runtime.halt() violation counter. */ public static void incrementExit() { EXIT_COUNTER.increment(); } - /** Increments the process-exec violation counter. */ public static void incrementExec() { EXEC_COUNTER.increment(); } - /** Returns the current file-access violation count. */ public static long fileCount() { return FILE_COUNTER.sum(); } - /** Returns the current network-connection violation count. */ public static long networkCount() { return NETWORK_COUNTER.sum(); } - /** Returns the current System.exit() / Runtime.halt() violation count. */ public static long exitCount() { return EXIT_COUNTER.sum(); } - /** Returns the current process-exec violation count. */ public static long execCount() { return EXEC_COUNTER.sum(); } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java index 5227c5e2aeb3..e4dce888126e 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java @@ -18,50 +18,21 @@ /** * Solr Security Agent — Java Security Manager replacement via ByteBuddy instrumentation. * - *

    This package contains the Java agent entry point and runtime enforcement infrastructure that - * replaces the removed Java Security Manager API. The agent intercepts file access, outbound - * network connections, {@code System.exit()}, {@code Runtime.halt()}, and process spawning at the - * bytecode level, enforcing a policy loaded from JDK-style {@code .policy} files. - * - *

    Key classes

    - * - *
      - *
    • {@link org.apache.solr.security.agent.SolrAgentEntryPoint} — {@code premain()}/{@code - * agentmain()} entry point; loads the policy and registers all ByteBuddy interceptors. - *
    • {@link org.apache.solr.security.agent.AgentPolicy} — immutable singleton holding the merged - * default + operator policy, enforcement mode, and trusted-host set. - *
    • {@link org.apache.solr.security.agent.PolicyLoader} — parses {@code .policy} files, - * performs variable substitution, and merges the default and operator extension files. - *
    • {@link org.apache.solr.security.agent.PolicyPropertyExpander} — expands {@code ${property}} - * placeholders; falls back to env vars using the standard {@code SOLR_FOO_BAR} convention - * (with custom overrides for non-standard mappings such as {@code SOLR_HOME}). - *
    • {@link org.apache.solr.security.agent.PolicyFileParser} — {@code StreamTokenizer}-based - * parser for JDK-style {@code .policy} files; handles comments and quoted strings natively. - *
    • {@link org.apache.solr.security.agent.StackCallerClassChainExtractor} — virtual-thread-safe - * call chain extraction via {@code StackWalker}. - *
    • {@link org.apache.solr.security.agent.SecurityViolationLogger} — structured SLF4J violation - * log emitter. - *
    • {@link org.apache.solr.security.agent.ViolationMetricsReporter} — per-type violation - * counters with deferred registration in {@code SolrMetricManager}. - *
    - * - *

    OpenSearch-derived files

    - * - *

    The following source files in this package were derived from the OpenSearch project (Apache License - * 2.0) and modified to integrate with Solr's policy model: - * - *

      - *
    • {@link org.apache.solr.security.agent.FileInterceptor} - *
    • {@link org.apache.solr.security.agent.SocketChannelInterceptor} - *
    • {@link org.apache.solr.security.agent.SystemExitInterceptor} - *
    • {@link org.apache.solr.security.agent.RuntimeHaltInterceptor} - *
    • {@link org.apache.solr.security.agent.StackCallerClassChainExtractor} - *
    • {@link org.apache.solr.security.agent.PolicyFileParser} - *
    • {@link org.apache.solr.security.agent.PolicyTokenStream} - *
    • {@link org.apache.solr.security.agent.PolicyPropertyExpander} - *
    - * - *

    See {@code NOTICE.txt} for full attribution. + *

    Intercepts file access, outbound network connections, {@code System.exit()}, {@code + * Runtime.halt()}, and process spawning at the bytecode level, enforcing a policy loaded from + * JDK-style {@code .policy} files. Entry point: {@link + * org.apache.solr.security.agent.SolrAgentEntryPoint}. Policy: {@link + * org.apache.solr.security.agent.AgentPolicy} (loaded by {@link + * org.apache.solr.security.agent.PolicyLoader}). + * + *

    Several files ({@link org.apache.solr.security.agent.FileInterceptor}, {@link + * org.apache.solr.security.agent.SocketChannelInterceptor}, {@link + * org.apache.solr.security.agent.SystemExitInterceptor}, {@link + * org.apache.solr.security.agent.RuntimeHaltInterceptor}, {@link + * org.apache.solr.security.agent.StackCallerClassChainExtractor}, {@link + * org.apache.solr.security.agent.PolicyFileParser}, {@link + * org.apache.solr.security.agent.PolicyTokenStream}, {@link + * org.apache.solr.security.agent.PolicyPropertyExpander}) were derived from the OpenSearch project + * (Apache License 2.0). See {@code NOTICE.txt} for attribution. */ package org.apache.solr.security.agent; From 1f7630c6a374156f9d8d560498e78da5fc1140a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sat, 30 May 2026 02:08:38 +0200 Subject: [PATCH 58/65] precommit --- .../apache/solr/security/agent/AgentPolicy.java | 8 ++++---- .../solr/security/agent/ApprovedCallSite.java | 10 ++++++++-- .../solr/security/agent/PolicyLoader.java | 17 ++++++++++------- .../security/agent/ProcessExecInterceptor.java | 4 +++- .../agent/SocketChannelInterceptor.java | 5 ++++- .../security/agent/SolrAgentEntryPoint.java | 5 ++++- 6 files changed, 33 insertions(+), 16 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java index cd74e5f4166a..716a636d25a7 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/AgentPolicy.java @@ -210,8 +210,8 @@ public boolean isExecApproved(String className) { } /** - * Returns {@code true} if any class in {@code chain} is approved to call {@code System.exit()} - * or {@code Runtime.halt()}. Any match anywhere in the chain is sufficient. + * Returns {@code true} if any class in {@code chain} is approved to call {@code System.exit()} or + * {@code Runtime.halt()}. Any match anywhere in the chain is sufficient. */ public boolean isChainThatCanExit(Collection> chain) { for (Class cls : chain) { @@ -226,8 +226,8 @@ public boolean isChainThatCanExit(Collection> chain) { } /** - * Returns {@code true} if any class in {@code chain} is approved to spawn child processes. - * Same semantics as {@link #isChainThatCanExit(Collection)}. + * Returns {@code true} if any class in {@code chain} is approved to spawn child processes. Same + * semantics as {@link #isChainThatCanExit(Collection)}. */ public boolean isChainThatCanExec(Collection> chain) { for (Class cls : chain) { diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java index 7853e8a7fe43..276047b9463d 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java @@ -77,7 +77,10 @@ public PolicySource source() { return source; } - /** Returns {@code true} if {@code className} matches the pattern; {@code false} for codeBase entries. */ + /** + * Returns {@code true} if {@code className} matches the pattern; {@code false} for codeBase + * entries. + */ public boolean matches(String className) { if (codeBase != null) return false; // codeBase entries must be checked via matchesCodeBase if ("*".equals(classNamePattern)) return true; @@ -88,7 +91,10 @@ public boolean matches(String className) { return classNamePattern.equals(className); } - /** Returns {@code true} if {@code cls} was loaded from this entry's codeBase; {@code false} for class-name entries. */ + /** + * Returns {@code true} if {@code cls} was loaded from this entry's codeBase; {@code false} for + * class-name entries. + */ public boolean matchesCodeBase(Class cls) { if (codeBase == null) return false; return SocketChannelInterceptor.isCallerFromCodeBase(List.of(cls), codeBase); diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java index 6199ae059560..93f40e9182f3 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java @@ -28,8 +28,9 @@ /** * Parses JDK-style {@code .policy} files with Solr variable substitution and produces an {@link * AgentPolicy}. Loads the mandatory default policy and an optional operator extension file (path - * from {@code solr.security.agent.extra.policy} or {@code ${server.dir}/etc/agent-security-extra.policy}). - * Unresolved {@code ${property}} placeholders fail fast at startup. + * from {@code solr.security.agent.extra.policy} or {@code + * ${server.dir}/etc/agent-security-extra.policy}). Unresolved {@code ${property}} placeholders fail + * fast at startup. */ public class PolicyLoader { @@ -87,7 +88,10 @@ public AgentPolicy load(Path defaultPolicyPath) { return buildPolicy(grants); } - /** Returns the extra-policy path, or {@code null} if neither sysprop nor {@code jetty.home} is set. */ + /** + * Returns the extra-policy path, or {@code null} if neither sysprop nor {@code jetty.home} is + * set. + */ static Path resolveExtraPolicyPath() { String explicitPath = PolicyPropertyExpander.getPropertyOrEnv("solr.security.agent.extra.policy"); @@ -106,10 +110,9 @@ static void parsePolicy(String content, PolicySource source, List ou } /** - * Parses grant blocks. Recognized permission types: - * {@code java.io.FilePermission} → {@link PermittedPath}, - * {@code java.net.SocketPermission} → {@link PermittedEndpoint}, - * {@code java.lang.RuntimePermission "exitVM"|"exec"} → {@link ApprovedCallSite}. + * Parses grant blocks. Recognized permission types: {@code java.io.FilePermission} → {@link + * PermittedPath}, {@code java.net.SocketPermission} → {@link PermittedEndpoint}, {@code + * java.lang.RuntimePermission "exitVM"|"exec"} → {@link ApprovedCallSite}. */ static void parsePolicyBlocks(String text, PolicySource source, List out) { List grantEntries; diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java index fb34aca673c4..09e77e9d9560 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ProcessExecInterceptor.java @@ -51,7 +51,9 @@ public static String deriveTarget(String methodName, Object[] args) { // Core check logic // --------------------------------------------------------------------------- - /** Checks the call chain against the approved exec-caller list; logs and throws in enforce mode. */ + /** + * Checks the call chain against the approved exec-caller list; logs and throws in enforce mode. + */ public static void checkExec(String target) { if (!AgentPolicy.isInitialized()) return; diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java index 65790aed4d58..64150190f4d2 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java @@ -67,7 +67,10 @@ public static void intercept(@Advice.AllArguments Object[] args) throws Exceptio // Shared enforcement helper // --------------------------------------------------------------------------- - /** Checks the policy for {@code host:port}; logs and throws {@link SecurityException} in enforce mode. */ + /** + * Checks the policy for {@code host:port}; logs and throws {@link SecurityException} in enforce + * mode. + */ static void enforceNetworkAccess(AgentPolicy policy, String host, int port, String caller) { if (!isEndpointPermitted(policy, host, port)) { String target = host + ":" + port; diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java index 4cae280058ad..7917ea13c4af 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java @@ -171,7 +171,10 @@ private static void installInterceptors(Instrumentation inst) { .installOn(inst); } - /** Resolves {@code server/etc/agent-security.policy} from {@code solr.install.dir} or {@code jetty.home}. */ + /** + * Resolves {@code server/etc/agent-security.policy} from {@code solr.install.dir} or {@code + * jetty.home}. + */ private static Path resolveDefaultPolicyPath() { String installDir = System.getProperty("solr.install.dir"); if (installDir != null && !installDir.isBlank()) { From 8a3b76f3a34308a01dbfe0711087219b10827aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sat, 30 May 2026 11:40:48 +0200 Subject: [PATCH 59/65] Fix SocketChannelInterceptor: make enforceNetworkAccess public Inlined ByteBuddy advice calls enforceNetworkAccess() from within sun.nio.ch.SocketChannelImpl (java.base). Package-private access causes IllegalAccessError at runtime; public is required. --- .../apache/solr/security/agent/SocketChannelInterceptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java index 64150190f4d2..f61af2080c2b 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java @@ -71,7 +71,7 @@ public static void intercept(@Advice.AllArguments Object[] args) throws Exceptio * Checks the policy for {@code host:port}; logs and throws {@link SecurityException} in enforce * mode. */ - static void enforceNetworkAccess(AgentPolicy policy, String host, int port, String caller) { + public static void enforceNetworkAccess(AgentPolicy policy, String host, int port, String caller) { if (!isEndpointPermitted(policy, host, port)) { String target = host + ":" + port; ViolationMetricsReporter.incrementNetwork(); From 0d51afd8101030b1af39fd8622f97a72a4892553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sat, 30 May 2026 11:42:08 +0200 Subject: [PATCH 60/65] Document ByteBuddy advice visibility rule in package-info.java Any static helper called from @Advice.OnMethodEnter must be public; the advice is inlined into JDK methods (java.base) and package-private access causes IllegalAccessError at runtime. --- .../java/org/apache/solr/security/agent/package-info.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java index e4dce888126e..2e7caedfd175 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java @@ -25,6 +25,12 @@ * org.apache.solr.security.agent.AgentPolicy} (loaded by {@link * org.apache.solr.security.agent.PolicyLoader}). * + *

    Advice visibility rule: Every static helper method called directly from an {@code + * @Advice.OnMethodEnter} body must be {@code public}. ByteBuddy inlines the advice bytecode into + * the target JDK method (e.g. {@code java.nio.file.Files}, {@code sun.nio.ch.SocketChannelImpl}), + * so the call site is in {@code java.base}. A package-private method in this package is + * inaccessible from there and causes {@code IllegalAccessError} at runtime. + * *

    Several files ({@link org.apache.solr.security.agent.FileInterceptor}, {@link * org.apache.solr.security.agent.SocketChannelInterceptor}, {@link * org.apache.solr.security.agent.SystemExitInterceptor}, {@link From f28ba3f4ed74d4886c46f16ec3f08dae3ece6949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sat, 30 May 2026 11:43:10 +0200 Subject: [PATCH 61/65] Tidy --- .../solr/security/agent/SocketChannelInterceptor.java | 3 ++- .../org/apache/solr/security/agent/package-info.java | 11 ++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java index f61af2080c2b..b986c6e94b83 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SocketChannelInterceptor.java @@ -71,7 +71,8 @@ public static void intercept(@Advice.AllArguments Object[] args) throws Exceptio * Checks the policy for {@code host:port}; logs and throws {@link SecurityException} in enforce * mode. */ - public static void enforceNetworkAccess(AgentPolicy policy, String host, int port, String caller) { + public static void enforceNetworkAccess( + AgentPolicy policy, String host, int port, String caller) { if (!isEndpointPermitted(policy, host, port)) { String target = host + ":" + port; ViolationMetricsReporter.incrementNetwork(); diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java index 2e7caedfd175..bac66a206f0f 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/package-info.java @@ -25,11 +25,12 @@ * org.apache.solr.security.agent.AgentPolicy} (loaded by {@link * org.apache.solr.security.agent.PolicyLoader}). * - *

    Advice visibility rule: Every static helper method called directly from an {@code - * @Advice.OnMethodEnter} body must be {@code public}. ByteBuddy inlines the advice bytecode into - * the target JDK method (e.g. {@code java.nio.file.Files}, {@code sun.nio.ch.SocketChannelImpl}), - * so the call site is in {@code java.base}. A package-private method in this package is - * inaccessible from there and causes {@code IllegalAccessError} at runtime. + *

    Advice visibility rule: Every static helper method called directly from an + * {@code @Advice.OnMethodEnter} body must be {@code public}. ByteBuddy inlines the advice bytecode + * into the target JDK method (e.g. {@code java.nio.file.Files}, {@code + * sun.nio.ch.SocketChannelImpl}), so the call site is in {@code java.base}. A package-private + * method in this package is inaccessible from there and causes {@code IllegalAccessError} at + * runtime. * *

    Several files ({@link org.apache.solr.security.agent.FileInterceptor}, {@link * org.apache.solr.security.agent.SocketChannelInterceptor}, {@link From 7a89fe827dcd90facbb1b03c73ace0aec95ca8e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sat, 30 May 2026 12:58:06 +0200 Subject: [PATCH 62/65] Move agent wiring from CoreContainer to CoreContainerProvider --- .../core/src/java/org/apache/solr/core/CoreContainer.java | 8 -------- .../org/apache/solr/servlet/CoreContainerProvider.java | 5 +++++ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java index 08b97abbfa60..186e319eae89 100644 --- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java +++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java @@ -145,8 +145,6 @@ import org.apache.solr.search.SolrCache; import org.apache.solr.search.SolrFieldCacheBean; import org.apache.solr.search.SolrIndexSearcher; -import org.apache.solr.security.AgentViolationBridge; -import org.apache.solr.security.AgentViolationMetrics; import org.apache.solr.security.AllowListUrlChecker; import org.apache.solr.security.AuditEvent; import org.apache.solr.security.AuditLoggerPlugin; @@ -953,12 +951,6 @@ private void loadInternal() { fieldCacheBean.initializeMetrics( solrMetricsContext, Attributes.of(CATEGORY_ATTR, SolrInfoBean.Category.CACHE.toString())); - // Register security agent violation metrics if the agent is loaded. - AgentViolationMetrics.register(metricManager, NODE_REGISTRY); - - // Wire security agent violations to SLF4J; no-op if agent JAR is absent. - AgentViolationBridge.wire(); - // setup executor to load cores in parallel coreLoadExecutor = solrMetricsContext.instrumentedExecutorService( diff --git a/solr/core/src/java/org/apache/solr/servlet/CoreContainerProvider.java b/solr/core/src/java/org/apache/solr/servlet/CoreContainerProvider.java index 19d06f392b7a..f0ad47d65946 100644 --- a/solr/core/src/java/org/apache/solr/servlet/CoreContainerProvider.java +++ b/solr/core/src/java/org/apache/solr/servlet/CoreContainerProvider.java @@ -46,6 +46,9 @@ import org.apache.solr.core.NodeConfig; import org.apache.solr.core.SolrCore; import org.apache.solr.core.SolrXmlConfig; +import org.apache.solr.metrics.SolrMetricManager; +import org.apache.solr.security.AgentViolationBridge; +import org.apache.solr.security.AgentViolationMetrics; import org.apache.solr.util.StartupLoggingUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -179,6 +182,8 @@ private void init(ServletContext servletContext) { }); coresInit = createCoreContainer(computeSolrHome(servletContext), extraProperties); + AgentViolationMetrics.register(coresInit.getMetricManager(), SolrMetricManager.NODE_REGISTRY); + AgentViolationBridge.wire(); if (log.isDebugEnabled()) { log.debug("user.dir={}", System.getProperty("user.dir")); From df1322e6063853a60e5ece8928b13ef3b3b8ddfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sat, 30 May 2026 12:58:10 +0200 Subject: [PATCH 63/65] Move SLF4J warning from Javadoc to build.gradle dependency comment --- solr/agent-sm/build.gradle | 4 ++++ .../org/apache/solr/security/agent/SolrAgentEntryPoint.java | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/solr/agent-sm/build.gradle b/solr/agent-sm/build.gradle index b51028a4fd42..a29e0e661c0d 100644 --- a/solr/agent-sm/build.gradle +++ b/solr/agent-sm/build.gradle @@ -24,6 +24,10 @@ dependencies { // Not included in the release so exclude from license/checksum checks. compileOnly libs.spotbugs.annotations + // !! DO NOT ADD SLF4J OR OTHER LOGGING JARS !! + // They are on Boot-Class-Path and initialise the logging binding before Log4j2 loads, + // permanently caching a NOP logger and preventing solr.log from being written. + // ByteBuddy for bytecode instrumentation - bundled into the fat agent JAR implementation libs.bytebuddy // byte-buddy-agent is the Java agent JAR itself; not directly imported in compiled classes diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java index 7917ea13c4af..d8732c0a6e0f 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/SolrAgentEntryPoint.java @@ -32,8 +32,7 @@ * Java agent entry point. Invoked by the JVM before the application main class when the agent JAR * is specified via {@code -javaagent:}. Loads the policy, initializes {@link AgentPolicy}, and * installs ByteBuddy interceptors. The agent JAR is on {@code Boot-Class-Path} so interceptor - * classes are visible to the bootstrap classloader; SLF4J is excluded from the fat JAR to avoid - * poisoning the binding before Log4j2 initializes. + * classes are visible to the bootstrap classloader. */ public final class SolrAgentEntryPoint { From 94d05e870b58485b2fb1a47453a1aee86ded0e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sat, 30 May 2026 13:44:32 +0200 Subject: [PATCH 64/65] Convert PermittedEndpoint, ApprovedCallSite, Raw*Permission to records --- .../solr/security/agent/ApprovedCallSite.java | 42 +----------------- .../security/agent/PermittedEndpoint.java | 43 ++----------------- .../solr/security/agent/PolicyLoader.java | 39 ++--------------- .../solr/security/agent/PolicyLoaderTest.java | 14 +++--- 4 files changed, 17 insertions(+), 121 deletions(-) diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java index 276047b9463d..e73bd2a95cb9 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/ApprovedCallSite.java @@ -26,7 +26,8 @@ * codeBase matching: the calling class must have been loaded from a location matching the JDK * policy {@code codeBase} URL ({@code file:/path/-} recursive, {@code file:/path/to.jar} exact). */ -public final class ApprovedCallSite { +public record ApprovedCallSite( + String classNamePattern, String codeBase, Operation operation, PolicySource source) { /** The restricted operation covered by this approval. */ public enum Operation { @@ -34,49 +35,10 @@ public enum Operation { EXEC } - private final String classNamePattern; // null when codeBase matching is used - private final String codeBase; // null when class-name matching is used - private final Operation operation; - private final PolicySource source; - ApprovedCallSite(String classNamePattern, Operation operation, PolicySource source) { this(classNamePattern, null, operation, source); } - ApprovedCallSite( - String classNamePattern, String codeBase, Operation operation, PolicySource source) { - this.classNamePattern = classNamePattern; - this.codeBase = codeBase; - this.operation = operation; - this.source = source; - } - - /** - * Fully-qualified class name or prefix pattern (ending in {@code .*}), or {@code null} when this - * entry uses codeBase matching instead. - */ - public String classNamePattern() { - return classNamePattern; - } - - /** - * JDK-style codeBase URL for code-source matching (e.g. {@code file:/opt/solr/modules/foo/-}), or - * {@code null} when this entry uses class-name matching instead. - */ - public String codeBase() { - return codeBase; - } - - /** The restricted operation this approval covers. */ - public Operation operation() { - return operation; - } - - /** Whether this entry came from the default bundled policy or an operator extension. */ - public PolicySource source() { - return source; - } - /** * Returns {@code true} if {@code className} matches the pattern; {@code false} for codeBase * entries. diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedEndpoint.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedEndpoint.java index 8d385b28fb8a..03ba4ab4eb50 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedEndpoint.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PermittedEndpoint.java @@ -27,44 +27,9 @@ *

    An optional {@code codeBase} restricts the grant to code loaded from a specific JAR location. * A {@code null} codeBase means the grant applies to all code. */ -public final class PermittedEndpoint { - - private final String hostPort; - private final String actions; - private final String codeBase; // null → applies to all code - private final PolicySource source; - - PermittedEndpoint(String hostPort, String actions, String codeBase, PolicySource source) { - this.hostPort = hostPort; - this.actions = actions != null ? actions : "connect,resolve"; - this.codeBase = codeBase; - this.source = source; - } - - /** - * Host-and-port string as written in the policy file (after variable substitution). Examples: - * {@code "localhost:8983"}, {@code "*:8983"}, {@code "127.0.0.1:1-65535"}. - */ - public String hostPort() { - return hostPort; - } - - /** Actions string, e.g. {@code "connect,resolve"}. */ - public String actions() { - return actions; - } - - /** - * The {@code codeBase} URL pattern this entry is scoped to, or {@code null} for a global grant. - * Used for pre-permitting bundled modules (e.g. jwt-auth) while keeping the grant out of reach of - * arbitrary code. - */ - public String codeBase() { - return codeBase; - } - - /** Whether this rule came from the default bundled policy or an operator extension. */ - public PolicySource source() { - return source; +public record PermittedEndpoint( + String hostPort, String actions, String codeBase, PolicySource source) { + public PermittedEndpoint { + if (actions == null) actions = "connect,resolve"; } } diff --git a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java index 93f40e9182f3..926aee0d95d6 100644 --- a/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java +++ b/solr/agent-sm/src/java/org/apache/solr/security/agent/PolicyLoader.java @@ -245,43 +245,12 @@ static class GrantBlock { } } - static class RawFilePermission { - final String target; - final String actions; - final PolicySource source; + record RawFilePermission(String target, String actions, PolicySource source) {} - RawFilePermission(String target, String actions, PolicySource source) { - this.target = target; - this.actions = actions; - this.source = source; - } - } + record RawSocketPermission( + String hostPort, String actions, String codeBase, PolicySource source) {} - static class RawSocketPermission { - final String hostPort; - final String actions; - final String codeBase; - final PolicySource source; - - RawSocketPermission(String hostPort, String actions, String codeBase, PolicySource source) { - this.hostPort = hostPort; - this.actions = actions; - this.codeBase = codeBase; - this.source = source; - } - } - - static class RawRuntimePermission { - final String type; // "exitVM" or "exec" - final String codeBase; - final PolicySource source; - - RawRuntimePermission(String type, String codeBase, PolicySource source) { - this.type = type; - this.codeBase = codeBase; - this.source = source; - } - } + record RawRuntimePermission(String type, String codeBase, PolicySource source) {} @SuppressForbidden( reason = diff --git a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java index 5bf6fa8b6268..6853d1ca4338 100644 --- a/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java +++ b/solr/agent-sm/src/test/org/apache/solr/security/agent/PolicyLoaderTest.java @@ -85,9 +85,9 @@ public void testGlobalGrantFilePermissionParsed() { PolicyLoader.GrantBlock block = blocks.get(0); assertNull(block.codeBase); assertEquals(1, block.filePaths.size()); - assertEquals("/solr/home/-", block.filePaths.get(0).target); - assertEquals("read", block.filePaths.get(0).actions); - assertEquals(PolicySource.DEFAULT, block.filePaths.get(0).source); + assertEquals("/solr/home/-", block.filePaths.get(0).target()); + assertEquals("read", block.filePaths.get(0).actions()); + assertEquals(PolicySource.DEFAULT, block.filePaths.get(0).source()); } @Test @@ -101,8 +101,8 @@ public void testGlobalGrantSocketPermissionParsed() { assertEquals(1, blocks.size()); assertEquals(1, blocks.get(0).socketPerms.size()); - assertEquals("*:8983", blocks.get(0).socketPerms.get(0).hostPort); - assertNull(blocks.get(0).socketPerms.get(0).codeBase); + assertEquals("*:8983", blocks.get(0).socketPerms.get(0).hostPort()); + assertNull(blocks.get(0).socketPerms.get(0).codeBase()); } @Test @@ -118,8 +118,8 @@ public void testCodeBaseScopedGrantParsed() { PolicyLoader.GrantBlock block = blocks.get(0); assertEquals("file:/opt/solr/modules/jwt-auth/-", block.codeBase); assertEquals(1, block.socketPerms.size()); - assertEquals("*", block.socketPerms.get(0).hostPort); - assertEquals("file:/opt/solr/modules/jwt-auth/-", block.socketPerms.get(0).codeBase); + assertEquals("*", block.socketPerms.get(0).hostPort()); + assertEquals("file:/opt/solr/modules/jwt-auth/-", block.socketPerms.get(0).codeBase()); } @Test From 8578fcdb90dc07a8389c8bcd6ae4099c43c6f681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sat, 30 May 2026 18:45:46 +0200 Subject: [PATCH 65/65] Bump bin-solr-test workflow timeout from 40 to 70 minutes --- .github/workflows/bin-solr-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bin-solr-test.yml b/.github/workflows/bin-solr-test.yml index 0e7369ad2cce..d4418340bcc4 100644 --- a/.github/workflows/bin-solr-test.yml +++ b/.github/workflows/bin-solr-test.yml @@ -19,7 +19,7 @@ jobs: name: Run Solr Script Tests runs-on: ubuntu-latest - timeout-minutes: 40 + timeout-minutes: 70 steps: - name: Checkout code