Skip to content

Conversation

jmendeza
Copy link
Member

@jmendeza jmendeza commented Sep 3, 2025

@jmendeza
Copy link
Member Author

jmendeza commented Sep 3, 2025

@coderabbitai review

Copy link

coderabbitai bot commented Sep 3, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

coderabbitai bot commented Sep 3, 2025

Summary by CodeRabbit

  • New Features
    • Introduced a composite whitelist mode that combines multiple whitelists and only permits operations approved by all, strengthening sandbox enforcement across method calls, constructors, field access, and certain environment lookups.
  • Tests
    • Added comprehensive unit tests and Groovy scripts/resources to verify allowed and restricted operations under the composite behavior.
  • Chores
    • Added JUnit and Mockito test dependencies.

Walkthrough

Introduces CompositeWhitelist, a new public class that wraps a non-empty, unmodifiable collection of Whitelist delegates and enforces unanimous approval: all permission checks (methods, constructors, static methods, field get/set, static field get/set, and isAllowedGetEnvSystemMethod) return true only if every delegate permits the action.

Changes

Cohort / File(s) Summary
Whitelist aggregation
src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/CompositeWhitelist.java
Added CompositeWhitelist extending Whitelist. Holds a protected, non-empty, unmodifiable Collection<? extends Whitelist> delegates (constructor throws IllegalArgumentException if empty). All permission checks (permitsMethod, permitsConstructor, permitsStaticMethod, permitsFieldGet, permitsFieldSet, permitsStaticFieldGet, permitsStaticFieldSet, and isAllowedGetEnvSystemMethod) are implemented by requiring all delegates to permit the action (allMatch). Uses @Nonnull/@CheckForNull annotations, CollectionUtils for emptiness check, and wraps delegates with unmodifiableCollection.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant CompositeWhitelist
    participant Delegate as Delegate[i]

    Caller->>CompositeWhitelist: permitsX(args)
    activate CompositeWhitelist
    loop for each delegate i in delegates
        CompositeWhitelist->>Delegate: permitsX(args)
        Delegate-->>CompositeWhitelist: boolean (perm_i)
    end
    alt unanimous approval required
        CompositeWhitelist-->>Caller: AND(perm_1..perm_N)
    end
    deactivate CompositeWhitelist

    note right of CompositeWhitelist: delegates is non-empty and unmodifiable
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@jmendeza
Copy link
Member Author

jmendeza commented Sep 4, 2025

@coderabbitai review

Copy link

coderabbitai bot commented Sep 4, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/CompositeWhitelist.java (3)

38-71: Confirm intended semantics (AND vs OR) and document

Class comment states AND semantics. That’s stricter than Jenkins’ common “union” behavior (permit if any delegate allows). If AND is truly desired, great—just ensure callers never pass an empty collection (addressed above). If union was intended, switch allMatch to anyMatch.

Example union change (only if required):

- return delegates.stream().allMatch(delegate -> delegate.permitsMethod(method, receiver, args));
+ return delegates.stream().anyMatch(delegate -> delegate.permitsMethod(method, receiver, args));

Apply similarly to the other permit methods if union semantics are desired.


27-31: Clarify Javadoc: explicitly state empty-delegates behavior

Once you enforce non-empty in the ctor, update the Javadoc to say “A call is permitted only if all delegates permit it. The delegate list must be non-empty.”


31-31: Add focused tests for safety and semantics

Please add unit tests covering:

  • Constructor rejects null, empty, and collections with null elements.
  • AND semantics: if any delegate denies, the composite denies; if all allow, it allows.
  • Regression test ensuring permissions are not granted when delegates list is emptied post-construction (immutability).

Do you want me to draft these tests?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5209181 and f548e06.

📒 Files selected for processing (1)
  • src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/CompositeWhitelist.java (1 hunks)
🔇 Additional comments (1)
src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/CompositeWhitelist.java (1)

18-26: Ensure annotation API dependency is declared
Confirm that javax.annotation.Nonnull/CheckForNull are provided at compile- and runtime (they’re not in Java 11+); add an explicit dependency on javax.annotation:javax.annotation-api or jakarta.annotation:jakarta.annotation-api in your POM if it isn’t already.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/CompositeWhitelist.java (1)

35-42: Fix mutability and vacuous-truth bypass: make delegates private final, defensively copy, and forbid null elements.

As written, unmodifiableCollection(delegates) is only a read-only view; external code can still mutate the backing collection to empty or inject nulls, turning allMatch(..) into allow-all (vacuous truth) or causing NPEs. Also the field is protected and non-final.

Apply within this range:

-public class CompositeWhitelist extends Whitelist {
-	protected Collection<? extends Whitelist> delegates;
+public class CompositeWhitelist extends Whitelist {
+	private final java.util.List<Whitelist> delegates;
@@
-	public CompositeWhitelist(Collection<? extends Whitelist> delegates) {
-		if (CollectionUtils.isEmpty(delegates)) {
-			throw new IllegalArgumentException("delegates must not be empty");
-		}
-		this.delegates = unmodifiableCollection(delegates);
-	}
+	public CompositeWhitelist(@Nonnull Collection<? extends Whitelist> delegates) {
+		java.util.Objects.requireNonNull(delegates, "delegates must not be null");
+		if (delegates.isEmpty()) {
+			throw new IllegalArgumentException("delegates must not be empty");
+		}
+		final java.util.ArrayList<Whitelist> copy = new java.util.ArrayList<>(delegates.size());
+		for (Whitelist d : delegates) {
+			copy.add(java.util.Objects.requireNonNull(d, "delegate must not be null"));
+		}
+		this.delegates = java.util.Collections.unmodifiableList(copy);
+	}

Also adjust imports outside this range:

// remove:
import org.apache.commons.collections4.CollectionUtils;
import static java.util.Collections.unmodifiableCollection;

// add:
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static java.util.Collections.unmodifiableList;
🧹 Nitpick comments (3)
src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/CompositeWhitelist.java (3)

30-33: Clarify Javadoc: AND for general checks, OR for getenv.

Current comment says “A call is permitted if all delegates permit it” but isAllowedGetEnvSystemMethod uses OR. Make the exception explicit.

-/**
- * Composite of multiple whitelists
- * A call is permitted if all delegates permit it
- */
+/**
+ * Composite of multiple whitelists.
+ * Most checks use consensus (AND): a call is permitted only if all delegates permit it.
+ * Exception: {@link #isAllowedGetEnvSystemMethod(Method, Object[])} uses OR so that any delegate
+ * allowing a specific System.getenv call is sufficient.
+ */

79-82: Confirm OR semantics for getenv are intentional.

This allows an env read if any delegate approves it, even if others would deny. If consensus is desired here too, switch to allMatch; otherwise keep OR and ensure docs/tests cover this policy.

Optional change:

-	return delegates.stream().anyMatch(delegate -> delegate.isAllowedGetEnvSystemMethod(m, args));
+	return delegates.stream().allMatch(delegate -> delegate.isAllowedGetEnvSystemMethod(m, args));

37-42: Optional: add a convenience varargs ctor.

Improves ergonomics at call sites without affecting behavior.

Example (outside this range):

public CompositeWhitelist(@Nonnull Whitelist... delegates) {
    this(Arrays.asList(Objects.requireNonNull(delegates, "delegates must not be null")));
}

I can add unit tests for AND vs OR behavior if helpful.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f548e06 and 09c4e01.

📒 Files selected for processing (1)
  • src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/CompositeWhitelist.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/CompositeWhitelist.java (1)
src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/Whitelist.java (1)
  • Whitelist (41-112)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Travis CI - Pull Request
🔇 Additional comments (1)
src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/CompositeWhitelist.java (1)

45-77: AND aggregation looks good and short-circuits correctly.

Use of allMatch(...) across delegates for methods/ctors/fields is correct and efficient.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (7)
src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/CompositeWhitelistTest.java (7)

29-31: Remove unused Mockito runner.

No mocks are used; drop the runner and imports to reduce test footprint.

-import org.junit.runner.RunWith;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)

Also applies to: 39-39


61-69: Always unregister the interceptor (use try/finally).

Prevents state leakage if the assertion or run fails.

 sandboxInterceptorEmptyWhitelist.register();
-assertThrows(INVALID_SCRIPT + " contains unsupported operations", UnsupportedOperationException.class, () ->
-        scriptEngine.run(INVALID_SCRIPT, new Binding()));
-sandboxInterceptorEmptyWhitelist.unregister();
+try {
+  assertThrows(INVALID_SCRIPT + " contains unsupported operations", UnsupportedOperationException.class,
+      () -> scriptEngine.run(INVALID_SCRIPT, new Binding()));
+} finally {
+  sandboxInterceptorEmptyWhitelist.unregister();
+}

71-77: Guard unregister with finally.

Ensures cleanup even on unexpected exceptions.

 sandboxInterceptor.register();
-scriptEngine.run(INVALID_SCRIPT, new Binding());
-sandboxInterceptor.unregister();
+try {
+  scriptEngine.run(INVALID_SCRIPT, new Binding());
+} finally {
+  sandboxInterceptor.unregister();
+}

79-91: Apply try/finally around both registrations.

Avoids interceptor leakage between tests.

 sandboxInterceptorEmptyWhitelist.register();
-assertThrows(MULTIPLE_OPS_SCRIPT + " contains unsupported operations", UnsupportedOperationException.class, () ->
-        scriptEngine.run(MULTIPLE_OPS_SCRIPT, new Binding()));
-sandboxInterceptorEmptyWhitelist.unregister();
+try {
+  assertThrows(MULTIPLE_OPS_SCRIPT + " contains unsupported operations", UnsupportedOperationException.class,
+      () -> scriptEngine.run(MULTIPLE_OPS_SCRIPT, new Binding()));
+} finally {
+  sandboxInterceptorEmptyWhitelist.unregister();
+}

 sandboxInterceptor.register();
-assertThrows(MULTIPLE_OPS_SCRIPT + " contains non-whitelisted operations. They should fail even if accepted" +
-        " by the blacklist", UnsupportedOperationException.class, () ->
-        scriptEngine.run(MULTIPLE_OPS_SCRIPT, new Binding()));
-sandboxInterceptor.unregister();
+try {
+  assertThrows(MULTIPLE_OPS_SCRIPT + " contains non-whitelisted operations. They should fail even if accepted by the blacklist",
+      UnsupportedOperationException.class, () -> scriptEngine.run(MULTIPLE_OPS_SCRIPT, new Binding()));
+} finally {
+  sandboxInterceptor.unregister();
+}

93-98: Prefer classpath-based script roots to file paths.

More robust across environments (e.g., IDEs, CI work dirs).

-	return new GroovyScriptEngine(SCRIPTS_PATH,
-			new GroovyClassLoader(getClass().getClassLoader(), compilerConfig));
+	ClassLoader cl = getClass().getClassLoader();
+	URL scriptsRoot = Objects.requireNonNull(cl.getResource("groovy/scripts/"), "Missing scripts resource");
+	return new GroovyScriptEngine(new URL[]{scriptsRoot},
+			new GroovyClassLoader(cl, compilerConfig));

Add import outside this hunk:

import java.net.URL;
import java.util.Objects;

100-107: Null-safe resource loading for clearer failures.

Avoids NPE if a resource is missing; yields actionable messages.

-	ClassLoader loader = getClass().getClassLoader();
-	Whitelist whitelist1 = new Blacklist(new InputStreamReader(loader.getResourceAsStream(BLACKLIST)));
-	Whitelist whitelist2 = new StaticWhitelist(new InputStreamReader(loader.getResourceAsStream(whitelist)));
+	ClassLoader loader = getClass().getClassLoader();
+	InputStream blIs = Objects.requireNonNull(loader.getResourceAsStream(BLACKLIST), "Missing resource: " + BLACKLIST);
+	InputStream wlIs = Objects.requireNonNull(loader.getResourceAsStream(whitelist), "Missing resource: " + whitelist);
+	Whitelist whitelist1 = new Blacklist(new InputStreamReader(blIs));
+	Whitelist whitelist2 = new StaticWhitelist(new InputStreamReader(wlIs));

Add import outside this hunk if not present:

import java.io.InputStream;
import java.util.Objects;

71-77: Optional: assert on failure cause to tighten intent.

Consider checking the reject message or method to ensure the composite logic (unanimous allow) is what fails, not a missing method.

Also applies to: 80-91

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a00e1d5 and 79a316c.

📒 Files selected for processing (7)
  • pom.xml (1 hunks)
  • src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/CompositeWhitelist.java (1 hunks)
  • src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/CompositeWhitelistTest.java (1 hunks)
  • src/test/resources/groovy/blacklist (1 hunks)
  • src/test/resources/groovy/scripts/invalid.groovy (1 hunks)
  • src/test/resources/groovy/scripts/multiple-ops.groovy (1 hunks)
  • src/test/resources/groovy/whitelist (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • src/test/resources/groovy/scripts/multiple-ops.groovy
  • src/test/resources/groovy/scripts/invalid.groovy
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/CompositeWhitelist.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/CompositeWhitelistTest.java (3)
src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/Whitelist.java (1)
  • Whitelist (41-112)
src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/blacklists/Blacklist.java (1)
  • Blacklist (31-72)
src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxInterceptor.java (1)
  • SuppressWarnings (57-550)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Travis CI - Pull Request
🔇 Additional comments (3)
src/test/resources/groovy/blacklist (1)

1-1: LGTM: blacklist entry format and target are correct.

This precisely blocks String.contains(CharSequence) for the composite tests.

pom.xml (1)

121-131: Pin test dependencies to explicit versions
Add <version>4.13.2</version> to JUnit and <version>5.13.0</version> to Mockito to ensure assertThrows support and JDK 21 compatibility. Verify the resolved versions with mvn dependency:tree (or your IDE’s dependency viewer) before merging.

src/test/resources/groovy/whitelist (1)

1-2: LGTM: whitelist entries align with test intents.

Allows Script.println(Object) and String.contains(CharSequence) for the positive path in CompositeWhitelist tests.

@jmendeza jmendeza marked this pull request as ready for review September 9, 2025 00:01
@jmendeza jmendeza requested a review from sumerjabri as a code owner September 9, 2025 00:01
@sumerjabri sumerjabri merged commit b6ee6ba into craftercms:support/4.x Sep 10, 2025
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants