Your Java service passes its dependency scan. The incident ticket closes. A week later, someone finds an older Log4j copy inside the deployed container, while the JavaScript client has never been scanned at all. This is how an apparently finished patch becomes an unfinished audit. For blue-team engineers, the job is to connect source dependencies, packaged artifacts, and running deployments across the whole application.
Your Audit Checkpoints
- Inventory Log4j in source, packaged artifacts, and deployed workloads.
- Verify all three CVEs before accepting a historical patch.
- Scan JavaScript clients and their build dependencies separately.
- Require successful scans before publishing either application layer.
- Retain reports tied to the exact release artifact.
Define the Application Boundary Before Scanning
Start with one application and its delivery path. Identify the Java service, browser client, build system, container image, and production deployment. Add scheduled jobs and supporting services that belong to the same release. Assign an owner to each component. A finding without an owner often survives several deployment cycles.
Record the repository revision and artifact identifier before collecting results. For containers, use an image digest. For an application archive, record its checksum. Tags and filenames can change without changing their apparent identity. Your evidence needs to identify the bytes that were checked.
Keep dependency presence, configuration exposure, and deployment status as separate fields. A vulnerable component deserves remediation even when a particular attack path appears blocked. Conversely, changing a dependency declaration does not prove that production received the replacement.
Find Every Log4j Copy That Can Reach Production
Read the Java dependency graph first. Maven and Gradle can show dependencies introduced by frameworks, libraries, and logging integrations. Check every module and relevant runtime configuration. Looking only at the top-level build file misses dependencies that arrive through other packages.
# Maven: inspect Log4j dependencies
./mvnw dependency:tree "-Dincludes=org.apache.logging.log4j:*"
# Gradle: inspect one service's runtime dependency selection
./gradlew :service:dependencyInsight \
--dependency log4j-core \
--configuration runtimeClasspath
Adjust the module path for your repository. Save the output with the audit record. For each Log4j component, capture the resolved version and the dependency path that introduced it. This tells the application owner where an upgrade or exclusion belongs.
Then inspect the deliverable. A dependency graph cannot account for every manually copied library, vendor package, or archive assembled outside the normal build. Your artifact scanner must inspect archives inside other archives and recognize shaded or relocated components where possible.
- Check executable JARs, WAR files, and application server library directories.
- Inspect final container filesystems and separately track vulnerable files retained in image layers.
- Include vendor agents, plugins, batch workers, and maintenance utilities.
- Review mounted libraries and startup scripts that modify the runtime classpath.
A filename search is useful for locating candidates. It is not reliable version evidence. Confirm identity through package metadata, archive contents, hashes, or a scanner that supports component identification. Renaming a JAR does not change its contents.
Read the Patch Timeline as Three Separate Checks
The first emergency upgrade was not the end of the patch sequence. Use vulnerability advisories to verify the affected component and fixed versions for each CVE.
CVE-2021-44228 concerns JNDI lookup behavior in vulnerable log4j-core releases. On the Java 8 release line, version 2.15.0 addressed the original issue.
CVE-2021-45046 exposed an incomplete fix in certain non-default configurations. Version 2.16.0 addressed that issue on the Java 8 line. Accepting 2.15.0 as the final patch would leave this check unresolved.
CVE-2021-45105 concerns uncontrolled recursion during lookup evaluation, which can cause denial of service. Version 2.17.0 addressed it on the Java 8 line. Historical fixes covering these three issues also reached 2.12.3 for Java 7 and 2.3.1 for Java 6.
These are historical boundaries, not current upgrade recommendations. Select a maintained, compatible release and check later advisories too. Track obsolete Java runtimes as migration work rather than treating an old backport as a permanent destination.
Do not label every logging package as affected by these three CVEs. Distinguish log4j-core from log4j-api and logging bridges. If your scanner reports only an API dependency, establish whether a core implementation also ships or loads at runtime.
Turn Version Findings Into Verified Remediation
Use a repeatable sequence for each affected service. This keeps patching evidence consistent across teams and prevents a build-file change from being mistaken for a completed deployment.
- Confirm the component. Record its version, archive location, dependency path, and deployment identifier.
- Select the replacement. Check Java compatibility, framework constraints, and applicable security advisories.
- Rebuild the application. Resolve dependencies again and recreate the archive or container from trusted inputs.
- Scan the new artifact. Confirm that old copies did not survive packaging or remain on the runtime classpath.
- Deploy and verify. Confirm that every running instance uses the approved artifact and has restarted where required.
Test logging behavior during this process. Check application startup, configured appenders, structured output, and log forwarding. A security patch that silently breaks logging weakens the team's ability to investigate later incidents.
During a rolling deployment, both old and new instances may serve traffic. Keep the finding open until the replacement reaches the entire intended workload. Include standby environments and rollback artifacts. An emergency rollback must not quietly restore a known vulnerable dependency.
Keep Containment and Incident Review Visible
Restricted outbound access and request filtering can reduce exposure while a patch is prepared. Treat these controls as temporary containment with an owner and expiry date. They do not establish that the dependency is fixed.
If the application was exposed while vulnerable, review the relevant period separately. Correlate suspicious log entries with outbound connections, process activity, and workload changes. A payload in a log is evidence of an attempt, not proof of successful execution. A clean dependency scan also cannot establish that earlier compromise never happened.
Extend the Audit to JavaScript Clients
Once Java-layer triage is under control, follow the application into the browser. Teams running Java backends often serve JavaScript clients carrying their own unscanned CVE exposure. A javascript scanner can support that frontend review alongside dependency analysis in the repository.
Keep the risk categories clear. Browser JavaScript does not acquire Log4Shell merely because it calls a Java service. Its dependencies can have separate vulnerabilities, and its build tools may run in Node.js with access to CI credentials or publishing systems.
Inspect the committed lockfile, resolved dependency tree, and built client. Include externally hosted scripts and manually copied libraries. A manifest describes intended dependency ranges. The lockfile records resolved packages. The deployed bundle shows what the browser actually receives.
- Scan direct and transitive dependencies across all frontend workspaces.
- Review development dependencies that execute during installation, testing, or bundling.
- Identify browser libraries loaded through script tags or copied into static directories.
- Confirm that approved bundles replace older files in hosting storage and caches.
Triage findings by advisory, affected version, dependency path, and execution context. A browser runtime issue and a build-server issue need different owners and tests. Both belong in the application audit.
Fix packages through reviewed changes. Check the resulting lockfile diff and run frontend tests. Avoid automatic dependency repair during a release job. It changes the input being released and can introduce compatibility problems without review.
Gate Both Layers Before Publishing a Release
Make the audit executable. Run Java and JavaScript checks for proposed changes, then repeat the artifact checks during release preparation. Publishing should depend on both layers passing. A scanner timeout, unavailable advisory feed, or unreadable report must fail the gate.
The following shell example fits a repository with backend and frontend directories. It uses strict failure handling and blocks npm findings at low severity or above. The audit exit rules explain how the selected threshold controls failure.
#!/usr/bin/env bash
set -euo pipefail
mkdir -p reports
(
cd backend
./mvnw -B verify
)
# Repository-owned wrapper around your approved Java scanner.
./ci/scan-java-artifacts.sh \
backend/target \
reports/java-scan.json
(
cd frontend
npm ci --ignore-scripts
npm audit --audit-level=low --json \
> ../reports/javascript-audit.json
# Run reviewed lifecycle hooks here if the build requires them.
npm test -- --run
npm run build
)
# Inspect the actual deliverables before permitting publication.
./ci/check-release-artifacts.sh \
backend/target \
frontend/dist \
reports
The two repository scripts are integration points, not standard scanner commands. Implement them with your chosen tools before enabling the job. The Java wrapper should inspect packaged dependencies, reject all three named Log4j CVEs regardless of a general severity threshold, and exit unsuccessfully on scan errors.
The release check should confirm that expected deliverables exist, inspect the frontend output for identifiable vulnerable libraries, and produce checksums. If production uses a container, build and scan that final image before publication too. Scanning a JAR alone cannot account for libraries added by the image build.
Using --ignore-scripts postpones dependency lifecycle scripts. Some projects require them to build successfully. Review those requirements and run approved hooks explicitly in an isolated job with minimal credentials.
Make Scan Results Required Deployment Inputs
Configure the publishing job to depend on the complete scan job. Disable settings that allow security failures to continue. If checks run separately, require both statuses before publication and deployment.
Retain reports even when a check fails. Store the commit, artifact checksum, scanner version, advisory-data timestamp, and decision. Restrict report access when dependency details or internal paths are sensitive.
Any accepted exception should name the advisory, component version, owner, rationale, and expiry date. Scope it narrowly. A broad suppression can hide the same vulnerable component after it reappears through another dependency path. Reject expired exceptions automatically.
Run scheduled rescans of released artifacts as well. A clean scan means no blocking findings were detected with the available data and coverage at that time. New advisories can change that result without any source change.
Close the Gap Between a Patched Service and a Checked Stack
A completed audit connects each production workload to verified release evidence. The Java service has an approved Log4j version. Its packaged and running copies agree. The JavaScript client has its own dependency results. The release pipeline requires both checks before publishing.
Start with one service and its frontend. Build that evidence chain, then apply the same workflow across the estate. The useful outcome is a release process that makes hidden copies, missed clients, and failed scans visible before they become another incident ticket.