All AEM as a Cloud Service skills: component development, Workflow model design, development, triggering, launchers, debugging, and triaging, Dispatcher config authoring, advisory, incident response, performance tuning, and security hardening, Rapid Development Environment (RDE) deployment, log inspection, snapshots, and troubleshooting via `aio aem rde`, Jackrabbit Oak query/index tuning (property, Lucene index definitions), and extracting/inventorying JCR queries (XPath, SQL2, QueryBuilder) across a codebase.
85%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
java.io.InputStream usage on AEM as a Cloud ServiceBPA pattern id: inputStream (Best Practices Analyzer code DG, subtype java.io.inputstream, importance ADVISORY). This is a migration reference, not a code-assessment pattern: BPA raises it for code carried over from AEM 6.x, AMS or on-premise. Detection is done by the deterministic scanner scripts/input-stream-runner.js (pure Node: no JDK, no rg, no LLM scan).
DG documentation: streaming binary data with java.io.InputStream can consume memory to the point of impacting performance, because containers on AEM as a Cloud Service have limited memory. The recommended solution is direct binary upload: aem-upload for assets, and the same pattern for other binaries.asset.getOriginal().getStream(), which downloads the binary onto the instance's ephemeral disk.So the finding is mainly about binaries passing through the JVM. BPA reports every use of the stream types in application code, though, so most sites are harmless and the skill's job is to find the risky ones. Closing streams (category B) is hygiene: worth doing, but it doesn't resolve the binary-streaming risk on its own.
In BPA reports, the other column names the stream type, for example java.io.InputStream, java.io.FileInputStream, java.io.ObjectInputStream or java.io.InputStreamReader.
The precedence is the same as for the other migration patterns:
migration-runbook.json, findingsByPattern.inputStream), when it exists.list-bpa-findings-by-pattern (AEM MCP Server; the deprecated fetch-cam-bpa-findings-by-pattern otherwise) with pattern: "inputStream", then a BPA CSV.inputStream findings (no BPA source, or none reported).BPA names classes, never lines, so the scanner runs in every case. Run it from the skill's scripts/ folder:
# CAM targets: write the tool's targets to a JSON file ([targets] or { "inputStream": [targets] })
node scripts/input-stream-runner.js <workspaceRoot> --bpa-json ./cam-input-stream.json --json > input-stream.json
# BPA CSV (used when there are no CAM targets)
node scripts/input-stream-runner.js <workspaceRoot> --csv ./reports/bpa.csv --json > input-stream.json
# Neither: the local scan of every src/main/java file
node scripts/input-stream-runner.js <workspaceRoot> --json > input-stream.jsonBoth options can be passed at once: CAM targets win over the CSV, and the CSV over the local scan. The JSON output's source field (cam, csv or local-scan) says which one was used.
The scanner:
Puts files in scope. With BPA rows, only the files of the reported classes. Nested classes (Foo$1) resolve to Foo.java, and stream usage elsewhere in the workspace is out of scope. Without BPA rows, every src/main/java/**/*.java file. Either way it skips src/test, target/ and node_modules, and comments and string literals can't produce matches.
Finds each stream acquisition site in those files and pre-classifies it (A–F, below) with a fix kind: decision, review, auto or none.
Emits one finding per file (the unit you edit), ordered by risk. Each finding carries sites: [{ line, snippet, categories, primary, fix, severity, source, variable, note, handoff? }] and fix: { decision, review, auto } totals. Files of BPA-reported classes are marked bpa.
Reports BPA rows it can't turn into file findings:
kind: "missing-source", plus a warning): the bundle has other classes in the workspace, or its packages share the workspace's package namespace. Fix it in the repository that builds it: open that repository and re-scan.kind: "platform-provided"): AEM Forms add-on bundles (adobe-aemfd-*, adobe-aemds-*, forms-foundation-*, xfa4j) or, for rows without a bundle name, its packages (com.adobe.fd, com.adobe.aemfd, com.adobe.aemds, com.adobe.xfa, com.adobe.pdfg, com.adobe.forms). On AEM as a Cloud Service, Adobe provides them when the Forms add-on is enabled in Cloud Manager (onboarding). Don't deploy or edit them.kind: "not-in-workspace"): other vendor code. Upgrade to a release that supports AEM as a Cloud Service, or replace it. For ACS AEM Commons, that's the acs-aem-commons-all package with the cloud classifier (setup). Never edit vendored code.kind: "bpa-unmatched"): BPA analyses compiled classes, so a use may not be visible in source (for example a stream passed straight from one library call to another). The hint names same-package files that use streams.Rows from a BPA CSV are grouped per bundle. CAM rows carry class names only, so they're grouped per package, and libraries embedded in a vendor or add-on bundle appear under their own package.
Rows with informational: true (benign-only files and the BPA-only kinds above) are not outstanding work. Report them, but don't build edit batches from them.
Batching: one batch is five files, taken from the non-informational findings in the scanner's order, so the highest-risk files come first.
The scanner's category is a lead, not the verdict. Read each site and confirm it before editing. Categories, highest priority first:
| # | Category | What the scanner looks for | Action | Fix kind |
|---|---|---|---|---|
| F | Java deserialization | new ObjectInputStream(…) or any *ObjectInputStream subclass (ValidatingObjectInputStream, …). Critical when the same method reads request, parameter, cookie, header or Base64 input. | If you own the format, replace it. If you don't (data written by AEM, a product feature or a library), add an ObjectInputFilter allow-list (R6). Never deserialize untrusted input. | decision |
| E | Binary streamed or processed in the JVM | DAM binary reads (asset.getOriginal().getStream(), rendition.getStream(), an asset resource adapted to InputStream); a repository binary copied to the response; a client upload written into the repository; asset processing in a workflow step; a stream in a method that starts a native process (ProcessBuilder, Runtime.exec, AEM's FFmpeg helpers). Other streams in the same class aren't marked E: the process is reported on the file's finding (nativeProcessLines) instead. | Deliver through the CDN, upload with direct binary upload, move asset processing to asset microservices (R5) | decision |
| D | Local filesystem | FileInputStream, Files.newInputStream, FileUtils.openInputStream on a hard-coded path, or without signs of per-request temp-file use (a temp file or directory, a working directory, the file deleted after use). The note lists local-filesystem writes in the same file. | Move the data and the code that writes it (R4) | decision |
| C | Unbounded in-memory read | Whole-stream reads (toByteArray, toString, readLines, readAllBytes, lines(), a copy into a ByteArrayOutputStream, parsing a request body) of a source with unknown size and no read limit. A Content-Length check is not a limit. Only reported when F, E and D don't apply, because their fixes replace the read. | Stream it (R2) or limit the read (R3) | review |
| B | Unclosed stream or undisposed JCR Binary | A stream opened in the method that isn't in try-with-resources, closed (directly or through a wrapper), returned, kept on a field, or handed to a callee that keeps it (a setter, map or builder), closes it (JCR createBinary) or is a same-file method that does either. A Binary from getBinary() that is never disposed. When the stream goes to an unknown callee, the note names it. An HTTP entity's content (entity.getContent()) belongs to the response, so an inline read of it isn't flagged. | Try-with-resources and Binary.dispose() (R1) | auto |
| A | Benign | Closed properly; a small bundled resource; ByteArrayInputStream; the container-owned request.getInputStream() with a read limit; a wrapper over the caller's stream; the type used only in a signature | No change. Record reviewed-no-change. | none |
Rules:
C+B). Its fix kind is the most demanding one.InputStreamReader / BufferedReader wrappers take the underlying stream's category.InputStream type. That only creates churn.config.author / config.publish, servlet registrations; workflow steps run on author). The right home for persisted data differs by tier (R4, R5).auto (B) is self-evident: apply R1 without asking. First confirm that the method really owns the stream (re-read the note when it names a callee).review (C) is self-evident only when a safe limit is clear from context (a config file, a JSON payload, an existing constant). Otherwise propose a limit and ask.decision (D, E, F) requires the user's decision: where the data lives, whether the client changes, which serialization format to use. Present the finding, the risk and a concrete proposal that fits the code's tier. Do not edit until the user confirms.Binary.dispose() (category B)// BEFORE
InputStream in = getClass().getResourceAsStream("/mapping.properties");
Properties mapping = new Properties();
mapping.load(in);// AFTER
Properties mapping = new Properties();
try (InputStream in = getClass().getResourceAsStream("/mapping.properties")) {
mapping.load(in);
}try (…) header, including wrappers (InputStreamReader, BufferedReader).request.getInputStream(). The caller or the container owns those.finally block (IOUtils.closeQuietly(in)), replace that with try-with-resources. Don't keep both.Binary from property.getBinary() must be disposed of as well:Binary binary = property.getBinary();
try (InputStream in = binary.getStream()) {
// …
} finally {
binary.dispose();
}Parse or copy from the stream rather than building a String or byte[] first. For example, for a JSON response from an external service:
// BEFORE: the whole body is read into a String before parsing
String body = IOUtils.toString(connection.getInputStream(), StandardCharsets.UTF_8);
Result result = objectMapper.readValue(body, Result.class);// AFTER: Jackson reads from the stream
Result result;
try (InputStream in = connection.getInputStream()) {
result = objectMapper.readValue(in, Result.class);
}Line-oriented data (CSV, redirect maps) can be processed one line at a time with a BufferedReader. Don't stream DAM binaries through the JVM at all: see R5.
When the whole payload is genuinely needed, read at most max + 1 bytes and reject anything larger, so oversized input fails instead of being silently cut off:
private static final int MAX_BYTES = 1024 * 1024; // tune to the real payload
static byte[] readAtMost(InputStream in, int maxBytes) throws IOException {
byte[] data = in.readNBytes(maxBytes + 1); // Java 11+
if (data.length > maxBytes) {
throw new IOException("Payload exceeds " + maxBytes + " bytes");
}
return data;
}-1 for chunked requests. Use it to reject early if you like, but still limit the read.BoundedInputStream alone is not enough. It stops at the limit without an error, so oversized input is silently truncated. If you use it, read one byte past the limit and reject.Binary.getSize() or Rendition.getSize() before reading (and dispose of the Binary, R1).@Designate configs.Find the code that writes the file first (the scanner lists writes in the same file; otherwise search the workspace for the path or its constant). That is the state that has to move.
| What the file holds | Move it to |
|---|---|
| Static data shipped with the code (lookup tables, templates, certificates without secrets) | A bundle resource (src/main/resources/…), read with getClass().getResourceAsStream("/…"), which is then category A |
| Environment-specific configuration | An OSGi config (ui.config). Secrets go to Cloud Manager via Branch A (osgi-cfg-json-cloud-manager.md) |
| Data written at runtime, on author | The repository: /conf/<app>/… (configuration), /var/<app>/… (operational data) or DAM (binaries) |
| Data written at runtime, on publish | An external store. Adobe's guidance is that the Publish tier ships data that must persist to an external service, and that publish instances share data through an external store; reverse replication to author isn't supported |
| Temporary files within a single request or job | File.createTempFile(…), deleted in finally. Not for huge files (they count against disk limits), and never relied on after the request |
Present the target location and how the data gets there (content package, repoinit, a one-time migration, or the external service's API), then wait for confirmation.
asset.getOriginal().getStream(), renditions): avoid them, per Adobe's guidelines.
BinaryDownload#getURI(BinaryDownloadOptions)) is an option. It returns null when the repository doesn't support direct download (for example the local SDK), so keep a fallback; the signed URL also bypasses the CDN.aem-upload). When the server code calls AssetManager.createAsset(path, InputStream, …) (sites with handoff: "asset-manager"), hand off to {code-assessment}/asset-manager/SKILL.md.JackrabbitValueFactory#initiateBinaryUpload returns upload URIs for the client; completeBinaryUpload(uploadToken) then returns the Binary to store. initiateBinaryUpload returns null when the repository doesn't support it (for example the local SDK), so keep a fallback.ProcessBuilder, Runtime.exec, AEM's FFmpeg helpers such as FFMpegWrapper): native binaries can't be deployed to AEM as a Cloud Service. For transcoding or other asset processing, move the work to asset microservices; for anything else, confirm the command exists on AEM as a Cloud Service or move the processing to an external service.ObjectInputStream (category F)ObjectMapper is available on AEM as a Cloud Service), or plain JCR properties when the data lives in the repository.ValidatingObjectInputStream with accept(…) does the same.ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"maxdepth=5;java.util.HashSet;java.lang.String;!*");
try (ObjectInputStream ois = new ObjectInputStream(in)) {
ois.setObjectInputFilter(filter);
Object value = ois.readObject();
}Make surgical, formatting-preserving edits. Do not reformat, reorder imports or change unrelated code.
sites, read the code and confirm or correct the scanner's category.java.nio.charset.StandardCharsets, org.apache.commons.io.IOUtils, javax.jcr.Binary) only when they are used. Remove imports that become unused.ok: false, exit code 2) when a requested file is missing or unreadable, naming it in error and stats.skippedFiles, so a mistyped path can't pass as clean; fix the path and re-run. Each fixed site must drop to A (or disappear), and the file's fix.auto total must reach 0:
node scripts/input-stream-runner.js <workspaceRoot> --files core/src/main/java/…/Foo.java,core/src/main/java/…/Bar.java --jsonreviewed-no-change, needs-decision proposals, and the remaining fix totals.fixed: <file>:<line> (<category>): recipe applied and verified by the re-scan.reviewed-no-change: <file>:<line>: confirmed A, or a scanner lead you rejected after reading the code (say why).needs-decision: <file>:<line> (<category>): D / E / F with a proposal for the code's tier, awaiting the user.missing-source bundle. Ask the user to open that repository, then re-run the scanner.not-in-workspace bundle, with its class and row counts. Upgrade or replace it; don't edit it.platform-provided bundle. Nothing to change in code: confirm the Forms add-on is enabled and that the AEM 6.5 Forms add-on package isn't part of the deployment.bpa-unmatched file, with its hint.--files re-scan of the touched files completed (ok: true, nothing in skippedFiles) and shows no remaining auto sites.Binary from getBinary() is disposed. No stream the method doesn't own is closed.ObjectInputStream sites are replaced, allow-listed, or reported as needs-decision (security-critical when the input is untrusted).mvn clean install passes for each touched module.InputStream for byte[] or a Reader makes memory use worse and changes no risk. Leave category A alone.BoundedInputStream to reject oversized input.request.getInputStream() or a parameter stream.IOUtils.closeQuietly in a finally after adding try-with-resources./tmp as storage. Temp files are per-instance, per-request scratch space only.For each changed production class, add or extend a unit test that pins the new behaviour. Match the module's existing JUnit 4/5 and AemContext setup (check its existing tests and pom.xml first; never mix JUnit versions):
ByteArrayInputStream in a small subclass that records close()), and a Binary is disposed.Do not generate tests for D / E / F until the user has confirmed the design.
DG documentationaem-upload, the direct binary upload client{code-assessment}/asset-manager/SKILL.md: AssetManager uploads and Direct Binary Access.tessl-plugin
skills
aem-rde
aem-workflow
code-assessment
content-distribution
create-component
assets
references
dispatcher
incident-response
references
dispatcher-foundation
performance-tuning
references
dispatcher-foundation
security-hardening
references
dispatcher-foundation
technical-advisory
references
dispatcher-foundation
workflow-orchestrator
references
dispatcher-foundation
ensure-agents-md
references
migration
references
dispatcher
legacy-ui
template-modernization
validation
scripts
dispatcher-tool
validate-migration