Class ClassloadFilterAudit

java.lang.Object
com.ultikits.ultitools.utils.ClassloadFilterAudit

final class ClassloadFilterAudit extends Object
GEN-07's observe-only classload filter audit (D-14): reproduces the exact matching semantics of the three name-based filter layers SecurityPolicy.isSafeClassName(String) used to enforce before D-12/D-13, and records what each removed layer WOULD have refused -- it refuses nothing itself.

Package-private on purpose. This class holds the four lists SecurityPolicy used to own directly (moved here once SecurityPolicy.addTrustedPackage(String) and SecurityPolicy.addDangerousClass(String) became no-ops, so these now reflect exactly what the framework as shipped would have judged, never a caller's runtime mutation). Same package is the low-surface choice: the alternative would have been making the lists public purely so a class outside utils could read them.

This evaluator never decides. classify(String) returns which layer would have refused a class name, or null for none -- never a boolean verdict, and nothing here can be used by a caller to refuse a class. It exists to answer, after release, "did removing these layers actually let anything through?" from real data rather than from an assertion.

Cannot reuse RemoteActionLog. UltiTools.java's bootstrap runs the classloading scan (initPluginModules()) before it constructs RemoteActionLog (initWebSocketManagers()) -- the log does not exist yet at the point this evaluator needs to run. Its Verdict is also ALLOWED/DENIED, and "would have been denied under the old gate but is now unconditionally allowed" is neither.

Logging (#557). Records go to a dedicated non-root Logger with setUseParentHandlers(false), so a record here never propagates on its own to the root logger that SystemLogHandler watches (that handler auto-reports any SEVERE record carrying a Throwable into ErrorReportCollector). A PluginLoggerBridge forwards INFO and above to the plugin's own logger, so an operator sees the line at its real level and in the server's normal log format. An earlier private console handler wrote to the standard error stream, which Paper prints as two WARN lines per record. What an operator sees, by design:

  • a clean result (no class would have been refused) is logged at FINE, below what is forwarded, and so is not shown -- the class-name filters no longer exist, so "nothing would have been refused" is not news;
  • a non-clean result is one INFO line per module jar, naming the module and the count.
The summary is emitted once per module jar, by the entity scan that visits every class of the jar; the main-class step does not audit, because the scan covers the main class too.

Locale.ROOT, not the default locale. The removed isSafeClassName lowercased with the no-argument String#toLowerCase(), which uses the JVM's default locale. Under a Turkish server locale, that method maps an ASCII capital I to a dotless ı, not a dotted i -- silently changing which class names match a SUSPICIOUS_KEYWORDS entry depending on where the server happens to run. This evaluator lowercases with Locale.ROOT instead, so a Turkish-locale server produces the same audit as an English-locale one. An audit whose result depends on the server's locale is not a measurement, so that latent defect is corrected here rather than carried into the telemetry meant to replace the removed enforcement.

Since:
6.3.0
  • Field Details

    • AUDIT_LOGGER

      private static final Logger AUDIT_LOGGER
    • FORWARD_LEVEL

      static final Level FORWARD_LEVEL
      The least severe level PluginLoggerBridge forwards to the plugin logger. Named, package-private and asserted on by ClassloadFilterAuditTest rather than left inline: the live handler list on a JUL logger is global mutable state that other tests in the same JVM add to and remove from, so a test that reads it back is order-dependent. This constant is the decision itself.

      It is INFO, not ALL, on purpose: record(java.lang.String, java.lang.String) logs FINE for every class classify(java.lang.String) returns a layer for, and that is every class of a third-party module (anything outside the seven trusted prefixes), up to a thousand per module. Forwarding those would print up to a thousand lines per module on top of the one INFO summary that is the operator-facing output. The logger itself stays at ALL so a test- or debug-attached handler still receives FINE.

    • SYSTEM_DANGEROUS_CLASSES

      static final Set<String> SYSTEM_DANGEROUS_CLASSES
    • DANGEROUS_PACKAGE_PREFIXES

      static final Set<String> DANGEROUS_PACKAGE_PREFIXES
    • TRUSTED_PACKAGE_PREFIXES

      static final Set<String> TRUSTED_PACKAGE_PREFIXES
    • SUSPICIOUS_KEYWORDS

      static final Set<String> SUSPICIOUS_KEYWORDS
    • COUNTS_BY_MODULE

      private static final Map<String,int[]> COUNTS_BY_MODULE
      One accumulator entry per module currently being scanned; reset when its summary emits.
  • Constructor Details

    • ClassloadFilterAudit

      private ClassloadFilterAudit()
  • Method Details

    • classify

      static ClassloadFilterAudit.Layer classify(String className)
      Classifies className against the four removed layers, in the exact order SecurityPolicy.isSafeClassName evaluated them before D-12/D-13 (exact blacklist, dangerous package prefix, trusted-package whitelist, suspicious keyword), and returns the FIRST layer that would have refused it -- or null if none would have.

      A null or blank className is not a filter layer and always classifies to null: the original isSafeClassName null branch is now subsumed by ClassLoaderUtils's VALID_CLASS_NAME_PATTERN format regex, which is the only thing that still rejects a malformed name (D-13).

      Parameters:
      className - the fully-qualified class name to classify, or null/blank
      Returns:
      the first layer that would have refused className, or null
    • record

      static void record(String moduleName, String className)
      Records one class evaluated during moduleName's scan. A no-op for a null or blank moduleName, or when classify(String) finds no layer would have refused className -- there is nothing interesting to record for a class none of the four removed layers would have touched.

      When a layer matches, increments that layer's per-module counter and logs the per-class detail at Level.FINE -- matching ModuleScanDiagnostics's recordSkippedClass/emitSummary shape (D-19), so the two diagnostics read as one pattern rather than two.

      This method never decides anything: it has no boolean return, and className is still handed to the real classloader by the caller regardless of what this method records (Test 10).

      Parameters:
      moduleName - the module (or scan unit) currently being evaluated
      className - the class name being evaluated, or null/blank
    • emitSummary

      static void emitSummary(String moduleName)
      Emits exactly ONE summary for moduleName: one Level.INFO line when at least one class would have been refused by a removed layer, or one Level.FINE line when none would (a clean result is the expected case, not operator news). Resets the accumulator for moduleName afterward, so a subsequent re-scan of the same module starts clean.

      This differs deliberately from ModuleScanDiagnostics.emitSummary, which emits nothing when its accumulator is empty: here a clean module is still a recorded measurement, just not one that is forwarded to the console.

      Parameters:
      moduleName - the module whose scan just finished
    • isBlank

      private static boolean isBlank(String value)