├── .gitignore ├── find-vulnerabilities ├── go.mod ├── helpers.go ├── version.go ├── go.sum ├── jarfile.go ├── README.md ├── fingerprint.go └── log4j.go ├── confirm-vulnerabilities ├── src │ └── main │ │ └── java │ │ └── com │ │ └── stripe │ │ └── log4j │ │ └── isitvuln │ │ ├── HostInfo.java │ │ ├── FileFormats.java │ │ ├── ProcessInfo.java │ │ ├── IsItVuln.java │ │ ├── IsItVulnAgent.java │ │ └── InspectedJVM.java ├── pom.xml ├── is-it-vulnerable.iml └── README.md ├── LICENSE ├── README.md └── CODE_OF_CONDUCT.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log -------------------------------------------------------------------------------- /find-vulnerabilities/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stripe/log4j-remediation-tools/find-vulnerabilities 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/hashicorp/go-version v1.3.0 // indirect 7 | github.com/shirou/gopsutil v3.21.11+incompatible 8 | github.com/tklauser/go-sysconf v0.3.9 // indirect 9 | github.com/yusufpapurcu/wmi v1.2.2 // indirect 10 | golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /confirm-vulnerabilities/src/main/java/com/stripe/log4j/isitvuln/HostInfo.java: -------------------------------------------------------------------------------- 1 | package com.stripe.log4j.isitvuln; 2 | 3 | import java.net.InetAddress; 4 | import java.net.UnknownHostException; 5 | import java.nio.file.Files; 6 | import java.nio.file.Paths; 7 | 8 | public class HostInfo { 9 | public static final String HOSTNAME = getHostname(); 10 | 11 | private static String getHostname() { 12 | InetAddress localHost; 13 | try { 14 | localHost = InetAddress.getLocalHost(); 15 | } catch (Exception e) { 16 | System.err.println("Could not get local host address"); 17 | e.printStackTrace(); 18 | return "unknown"; 19 | } 20 | try { 21 | return localHost.getHostName(); 22 | } catch (Exception e) { 23 | System.err.println("Could not get local host name"); 24 | e.printStackTrace(); 25 | return localHost.toString(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /find-vulnerabilities/helpers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "io" 7 | "io/fs" 8 | "os" 9 | ) 10 | 11 | func hashFile(path string) string { 12 | f, err := os.Open(path) 13 | if err != nil { 14 | return "error" 15 | } 16 | defer f.Close() 17 | 18 | return hashFsFile(f) 19 | } 20 | 21 | func hashFsFile(f fs.File) string { 22 | hasher := sha256.New() 23 | if _, err := io.Copy(hasher, f); err != nil { 24 | return "error" 25 | } 26 | 27 | return hex.EncodeToString(hasher.Sum(nil)) 28 | } 29 | 30 | func fileExists(path string) bool { 31 | info, err := os.Lstat(path) 32 | 33 | switch { 34 | case os.IsNotExist(err): 35 | // path does not exist 36 | return false 37 | case err != nil: 38 | // return true since error is not of type IsNotExist 39 | return true 40 | default: 41 | // return true only if this is a file 42 | return info.Mode().IsRegular() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /find-vulnerabilities/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/hashicorp/go-version" 7 | ) 8 | 9 | var ( 10 | version2 = version.Must(version.NewVersion("2.0.0")) 11 | flagVersion = version.Must(version.NewVersion("2.10.0")) 12 | fixedVersion = version.Must(version.NewVersion("2.16.0")) 13 | ) 14 | 15 | func parseVersion(ver string) (*version.Version, error) { 16 | // Add any custom version parsing logic here 17 | return version.NewVersion(ver) 18 | } 19 | 20 | func isPatchedVersion(ver *version.Version) bool { 21 | // If the version is newer than the fixed version, we're good. 22 | if ver.GreaterThanOrEqual(fixedVersion) { 23 | if *verbose { 24 | log.Printf("not vulnerable: version newer") 25 | } 26 | return true 27 | } 28 | 29 | // Add special cases here 30 | switch ver.Original() { 31 | // e.g. case "my version": 32 | // return true 33 | } 34 | 35 | return false 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021- Stripe, Inc. (https://stripe.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /confirm-vulnerabilities/src/main/java/com/stripe/log4j/isitvuln/FileFormats.java: -------------------------------------------------------------------------------- 1 | package com.stripe.log4j.isitvuln; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | public class FileFormats { 7 | public static final List SYSPROPS_FOR_CSV = Arrays.asList( 8 | "log4j2.formatMsgNoLookups", 9 | "com.sun.jndi.rmi.object.trustURLCodebase", 10 | "com.sun.jndi.cosnaming.object.trustURLCodebase", 11 | "com.sun.jndi.ldap.object.trustURLCodebase"); 12 | 13 | public static String toCsv(String... cells) { 14 | StringBuilder sb = new StringBuilder(); 15 | boolean first = true; 16 | for (String cell : cells) { 17 | if (!first) { 18 | sb.append(','); 19 | } 20 | first = false; 21 | sb.append(escapeSpecialCharacters(cell)); 22 | } 23 | return sb.toString(); 24 | } 25 | 26 | public static String escapeSpecialCharacters(String data) { 27 | if (data == null) { 28 | return "null"; 29 | } 30 | String escapedData = data.replaceAll("\\R", " "); 31 | if (data.contains(",") || data.contains("\"") || data.contains("'")) { 32 | data = data.replace("\"", "\"\""); 33 | escapedData = "\"" + data + "\""; 34 | } 35 | return escapedData; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /find-vulnerabilities/go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 2 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 3 | github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= 4 | github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 5 | github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= 6 | github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 7 | github.com/tklauser/go-sysconf v0.3.9 h1:JeUVdAOWhhxVcU6Eqr/ATFHgXk/mmiItdKeJPev3vTo= 8 | github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs= 9 | github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ= 10 | github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8= 11 | github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= 12 | github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 13 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 14 | golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 15 | golang.org/x/sys v0.0.0-20211210111614-af8b64212486 h1:5hpz5aRr+W1erYCL5JRhSUBJRph7l9XkNveoExlrKYk= 16 | golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 17 | -------------------------------------------------------------------------------- /confirm-vulnerabilities/src/main/java/com/stripe/log4j/isitvuln/ProcessInfo.java: -------------------------------------------------------------------------------- 1 | package com.stripe.log4j.isitvuln; 2 | 3 | import sun.management.VMManagement; 4 | 5 | import java.lang.management.ManagementFactory; 6 | import java.lang.management.RuntimeMXBean; 7 | import java.lang.reflect.Field; 8 | import java.lang.reflect.Method; 9 | import java.net.URISyntaxException; 10 | 11 | public class ProcessInfo { 12 | public static final String THIS_JAR_PATH = getThisJarPath(); 13 | public static final int MY_PID = getMyPidNoThrow(); 14 | 15 | public static String getThisJarPath() { 16 | try { 17 | return IsItVuln.class.getProtectionDomain().getCodeSource().getLocation() 18 | .toURI().getPath(); 19 | } catch (URISyntaxException e) { 20 | throw new RuntimeException(e); 21 | } 22 | } 23 | 24 | private static int getMyPidNoThrow() { 25 | int pid = -1; 26 | try { 27 | pid = getMyPid(); 28 | } catch (Exception e) { 29 | System.err.println("WARN: could not get my pid"); 30 | e.printStackTrace(); 31 | } 32 | return pid; 33 | } 34 | 35 | private static int getMyPid() throws Exception { 36 | 37 | RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean(); 38 | Field jvm = runtime.getClass().getDeclaredField("jvm"); 39 | jvm.setAccessible(true); 40 | 41 | VMManagement management = (VMManagement) jvm.get(runtime); 42 | Method method = management.getClass().getDeclaredMethod("getProcessId"); 43 | method.setAccessible(true); 44 | 45 | return (Integer) method.invoke(management); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /find-vulnerabilities/jarfile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/zip" 5 | "bufio" 6 | "log" 7 | "strings" 8 | ) 9 | 10 | func versionFromJARArchive(r *zip.Reader) string { 11 | if ver := versionFromJARArchiveFingerprint(r); ver != "unknown" { 12 | return ver 13 | } 14 | if ver := versionFromJARArchiveMeta(r); ver != "unknown" { 15 | return ver 16 | } 17 | 18 | return "unknown" 19 | } 20 | 21 | func versionFromJARArchiveFingerprint(r *zip.Reader) string { 22 | for _, fp := range log4jFingerprints { 23 | f, err := r.Open(fp.file) 24 | if err != nil { 25 | continue 26 | } 27 | defer f.Close() 28 | 29 | hash := hashFsFile(f) 30 | if hash == fp.sha256 { 31 | if *verbose { 32 | log.Printf("found log4j version %q by fingerprint", fp.version) 33 | } 34 | return fp.version 35 | } 36 | } 37 | 38 | return "unknown" 39 | } 40 | 41 | func versionFromJARArchiveMeta(r *zip.Reader) string { 42 | f, err := r.Open("META-INF/MANIFEST.MF") 43 | if err != nil { 44 | return "unknown" 45 | } 46 | defer f.Close() 47 | 48 | metadata := make(map[string]string) 49 | scanner := bufio.NewScanner(f) 50 | for scanner.Scan() { 51 | parts := strings.SplitN(scanner.Text(), ": ", 2) 52 | if len(parts) == 2 { 53 | metadata[parts[0]] = parts[1] 54 | } else { 55 | // Noisy 56 | //log.Printf("invalid manifest line: %q", scanner.Text()) 57 | } 58 | } 59 | if err := scanner.Err(); err != nil { 60 | log.Printf("error reading manifest file: %w", err) 61 | return "unknown" 62 | } 63 | 64 | candidates := []string{"Implementation-Version", "Bundle-Version"} 65 | for _, candidate := range candidates { 66 | if s, ok := metadata[candidate]; ok { 67 | return s 68 | } 69 | } 70 | 71 | return "unknown" 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `log4j-remediation-tools` 2 | 3 | > Tools for finding and reproducing the [CVE-2021-44228](https://nvd.nist.gov/vuln/detail/CVE-2021-44228) `log4j2` vulnerability 4 | 5 | ## Tools 6 | 7 | - [`find-vulnerabilities`](./find-vulnerabilities): determine heuristically whether a running JVM is vulnerable 8 | - [`confirm-vulnerabilities`](./confirm-vulnerabilities): determine with 100% accuracy whether a running JVM is vulnerable 9 | 10 | ## Usage 11 | 12 | Both of these tools scan all running JVM processes on a machine, and produce a CSV report about which processes may be / are vulnerable. 13 | 14 | Check out the corresponding READMEs for [`find-vulnerabilities/`](./find-vulnerabilities) and [`confirm-vulnerabilities/`](./confirm-vulnerabilities) for usage details. 15 | 16 | ### Which tool should I use? 17 | 18 | Here are a few tradeoffs to help you determine which tool is right for your use case: 19 | 20 | `find-vulnerabilities` is low-risk to run, but has the possibility of missing: 21 | 22 | - Cases where a system property is not set on the CLI, e.g. at runtime 23 | - Cases where the JVM has closed the file descriptor for the jar 24 | - Non-standard / patched releases of `log4j2` 25 | 26 | `confirm-vulnerabilities` uses the JVM Attach API which: 27 | 28 | - May not work if an application explicitly disables this API 29 | - May crash the running JVM due to JVM bugs 30 | - May briefly slow down the running JVM while waiting for JVM pause 31 | 32 | ## Contributing 33 | 34 | This project welcomes feedback and contributions; however, we might be slow to respond to or triage your requests. We appreciate your patience. 35 | 36 | ## License 37 | 38 | This project uses the [MIT license](LICENSE.md). 39 | 40 | ## Code of conduct 41 | 42 | This project has adopted the Stripe [Code of conduct](CODE_OF_CONDUCT.md). 43 | -------------------------------------------------------------------------------- /confirm-vulnerabilities/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.stripe.log4j 8 | is-it-vulnerable 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 1.8 13 | 1.8 14 | 15 | 16 | 17 | 18 | org.apache.logging.log4j 19 | log4j-core 20 | 2.14.1 21 | 22 | 23 | org.apache.maven.plugins 24 | maven-compiler-plugin 25 | 3.8.1 26 | 27 | 28 | 29 | 30 | 31 | 32 | org.apache.maven.plugins 33 | maven-shade-plugin 34 | 3.2.4 35 | 36 | 37 | package 38 | 39 | shade 40 | 41 | 42 | 43 | 45 | 46 | com.stripe.log4j.isitvuln.IsItVuln 47 | 123 48 | com.stripe.log4j.isitvuln.IsItVulnAgent 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /confirm-vulnerabilities/src/main/java/com/stripe/log4j/isitvuln/IsItVuln.java: -------------------------------------------------------------------------------- 1 | package com.stripe.log4j.isitvuln; 2 | 3 | import sun.jvmstat.monitor.MonitoredHost; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.concurrent.ExecutorService; 11 | import java.util.concurrent.Executors; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | public class IsItVuln { 15 | 16 | public static final String VERSION_NUMBER = "1"; 17 | 18 | private static final int TIMEOUT_SECS = 60; 19 | 20 | public static void main(String[] args) throws InterruptedException { 21 | System.err.println("Will use this jar for agent: " + ProcessInfo.THIS_JAR_PATH); 22 | Path temp = createTempDir(); 23 | 24 | ExecutorService inspectionThreads = Executors.newCachedThreadPool(); 25 | 26 | printAtomic("date,host,tool,version,pid,path,jre,log4j,log4j version,formatMsgNoLookups,ldap trustURLCodebase,rmi trustURLCodebase,cosnaming trustURLCodebase,exploited"); 27 | List jvms = new ArrayList<>(); 28 | try { 29 | MonitoredHost host = MonitoredHost.getMonitoredHost((String) null); 30 | for (int pid : host.activeVms()) { 31 | if (pid == ProcessInfo.MY_PID) { 32 | continue; 33 | } 34 | Path outputFile = temp.resolve("result-" + pid + ".csv"); 35 | InspectedJVM inspectedJVM = new InspectedJVM(pid, host, outputFile, IsItVuln::printAtomic); 36 | jvms.add(inspectedJVM); 37 | inspectionThreads.submit(inspectedJVM::inspect); 38 | } 39 | } catch (Exception e) { 40 | e.printStackTrace(); 41 | System.exit(1); 42 | } 43 | 44 | waitForAllVmInspectionsToComplete(jvms, inspectionThreads); 45 | } 46 | 47 | private synchronized static void printAtomic(String x) { 48 | System.out.println(x); 49 | } 50 | 51 | 52 | private static void waitForAllVmInspectionsToComplete(List jvms, ExecutorService executor) throws InterruptedException { 53 | long startNano = System.nanoTime(); 54 | jvms.forEach(InspectedJVM::pleaseFinish); 55 | executor.shutdown(); 56 | while (!executor.isTerminated()) { 57 | if (TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startNano) >= TIMEOUT_SECS) { 58 | System.err.println("Exiting after " + TIMEOUT_SECS + " sec timeout"); 59 | System.exit(18); 60 | return; 61 | } 62 | Thread.sleep(1000); 63 | } 64 | } 65 | 66 | private static Path createTempDir() { 67 | Path path; 68 | try { 69 | path = Files.createTempDirectory("is-it-vuln").toAbsolutePath(); 70 | } catch (IOException e) { 71 | System.err.println("Could not create tmpdir"); 72 | System.exit(8); 73 | throw new RuntimeException(e); 74 | } 75 | return path; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /find-vulnerabilities/README.md: -------------------------------------------------------------------------------- 1 | # Find vulnerabilities 2 | 3 | > Determine heuristically whether a running JVM is vulnerable 4 | 5 | ## Building 6 | 7 | Cross compile for linux with: 8 | 9 | ```sh 10 | env GOOS=linux GOARCH=amd64 go build -o log4j-finder-amd64-linux *.go 11 | ``` 12 | 13 | ## Running 14 | 15 | Run with sudo: 16 | 17 | ```sh 18 | sudo ./log4j-finder-amd64-linux 19 | ``` 20 | 21 | Add verbose for more logging: 22 | 23 | ```sh 24 | sudo ./log4j-finder-amd64-linux -verbose 25 | ``` 26 | 27 | ### Example output 28 | 29 | Sample output to `stdout`: 30 | 31 | ```csv 32 | hostname,tool,tool_sha,pid,java_bin_location,java_version,prop1,prop2,prop3,prop4,using_log4j,oldest_log4j_version,vulnerable,oldest_vulnerable_log4j_version 33 | myhost.stripe.com,lite,5312d3ca2e10757078770b735c83088820627f3cdcb34f3df8d99d16dfe00903,1234,/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java,,,,,,true,2.1,yes,2.1 34 | myhost.stripe.com,lite,5312d3ca2e10757078770b735c83088820627f3cdcb34f3df8d99d16dfe00903,5678,/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java,,,,,,true,2.9.0,yes,2.9.0 35 | myhost.stripe.com,lite,5312d3ca2e10757078770b735c83088820627f3cdcb34f3df8d99d16dfe00903,9999,/usr/lib/jvm/java-11-openjdk-amd64/bin/java,,,,,,true,2.16.0,no,2.16.0 36 | 2021/12/14 23:57:18 done 37 | ``` 38 | 39 | | hostname | tool | tool_sha | pid | java_bin_location | java_version | prop1 | prop2 | prop3 | prop4 | using_log4j | oldest_log4j_version | vulnerable | oldest_vulnerable_log4j_version | 40 | | ----------------- | ---- | ---------------------------------------------------------------- | ---- | ---------------------------------------------- | ------------ | ----- | ----- | ----- | ----- | ----------- | -------------------- | ---------- | ------------------------------- | 41 | | myhost.stripe.com | lite | 5312d3ca2e10757078770b735c83088820627f3cdcb34f3df8d99d16dfe00903 | 1234 | /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java | | | | | | true | 2.1 | yes | 2.1 | 42 | | myhost.stripe.com | lite | 5312d3ca2e10757078770b735c83088820627f3cdcb34f3df8d99d16dfe00903 | 5678 | /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java | | | | | | true | 2.9.0 | yes | 2.9.0 | 43 | | myhost.stripe.com | lite | 5312d3ca2e10757078770b735c83088820627f3cdcb34f3df8d99d16dfe00903 | 9999 | /usr/lib/jvm/java-11-openjdk-amd64/bin/java | | | | | | true | 2.16.0 | no | 2.16.0 | 44 | 45 | ## How it works 46 | 47 | This tools scans all open processes and attempts to find running JVMs. For 48 | running JVMs, it then looks through all open file descriptors to identify 49 | loaded JAR files and heuristically determine whether log4j is present, and if 50 | so, what version is running. 51 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at conduct@stripe.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /confirm-vulnerabilities/src/main/java/com/stripe/log4j/isitvuln/IsItVulnAgent.java: -------------------------------------------------------------------------------- 1 | package com.stripe.log4j.isitvuln; 2 | 3 | import java.io.FileWriter; 4 | import java.io.IOException; 5 | import java.io.PrintWriter; 6 | import java.lang.instrument.Instrumentation; 7 | import java.lang.reflect.Method; 8 | import java.nio.file.Files; 9 | import java.nio.file.Paths; 10 | import java.util.Properties; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | 13 | import static java.nio.file.StandardCopyOption.ATOMIC_MOVE; 14 | import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; 15 | 16 | public class IsItVulnAgent { 17 | private final static AtomicInteger runs = new AtomicInteger(0); 18 | 19 | public static void agentmain(String agentArgs, Instrumentation inst) throws Throwable { 20 | try { 21 | String[] args = agentArgs.split(" "); 22 | String version = args[0]; 23 | 24 | if (!version.equals(IsItVuln.VERSION_NUMBER)) { 25 | throw new IllegalStateException( 26 | String.format( 27 | "agent is running old version of code and force reloading classes is not implemented (tool version %s != agent version %s)", 28 | version, IsItVuln.VERSION_NUMBER)); 29 | } 30 | int previousRuns = runs.getAndIncrement(); 31 | if (previousRuns != 0) { 32 | System.err.printf( 33 | "WARN: this is IsItVuln agent run #%d without reloading classes, though we appear to be on the right version (%s)%n", 34 | previousRuns + 1, version); 35 | } 36 | 37 | int port = Integer.parseInt(args[1]); 38 | String outputFile = args[2]; 39 | 40 | writePropertiesFile(outputFile, populateProperties(inst, port)); 41 | } catch (Throwable e) { 42 | System.err.println("IsItVuln agent failed"); 43 | e.printStackTrace(); 44 | throw e; 45 | } 46 | } 47 | 48 | private static Properties populateProperties(Instrumentation inst, int port) { 49 | Properties result = new Properties(); 50 | 51 | checkLog4j(inst, port, result); 52 | 53 | for (String name : FileFormats.SYSPROPS_FOR_CSV) { 54 | result.put(name, System.getProperty(name, "unset")); 55 | } 56 | return result; 57 | } 58 | 59 | private static void writePropertiesFile(String outputFile, Properties result) throws IOException { 60 | String tmpFile = outputFile + ".tmp"; 61 | try (PrintWriter out = new PrintWriter(new FileWriter(tmpFile))) { 62 | result.store(out, null); 63 | } 64 | Files.move(Paths.get(tmpFile), Paths.get(outputFile), ATOMIC_MOVE, REPLACE_EXISTING); 65 | } 66 | 67 | private static void checkLog4j(Instrumentation inst, int port, Properties result) { 68 | result.put("log4j", "none"); 69 | result.put("hasLog4j", "false"); 70 | for (Class cls : inst.getAllLoadedClasses()) { 71 | if (cls.getName().equals("org.apache.logging.log4j.Logger")) { 72 | String ver = cls.getPackage().getImplementationVersion(); 73 | if (!result.containsKey("log4j") || result.getProperty("log4j").equals("unknown")) { 74 | result.put("log4j", ver != null ? ver : "unknown"); 75 | } 76 | result.put("hasLog4j", "true"); 77 | } else if (cls.getName().equals("org.apache.logging.log4j.LogManager")) { 78 | tryLoggingExploitString(cls, port); 79 | } 80 | } 81 | } 82 | 83 | private static void tryLoggingExploitString(Class cls, int port) { 84 | try { 85 | Object logger = cls.getMethod("getLogger", Class.class).invoke(null, IsItVulnAgent.class); 86 | Method errorMethod = logger.getClass().getMethod("error", String.class); 87 | String logLine = String.format("IsItVuln ExploitAttempt ${jndi:ldap://127.0.0.1:%d/x}", port); 88 | System.err.println("Attempting to log " + logLine); 89 | errorMethod.invoke(logger, logLine); 90 | } catch (Exception e) { 91 | e.printStackTrace(); 92 | //TODO: log this error to file 93 | } 94 | //TODO: serious failure if that didn't work! 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /confirm-vulnerabilities/is-it-vulnerable.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /confirm-vulnerabilities/README.md: -------------------------------------------------------------------------------- 1 | # Confirm vulnerabilities 2 | 3 | > Determine with 100% accuracy whether a running JVM is vulnerable 4 | 5 | Authoritatively scans all running JVM's for the December 2021 Log4j exploit [CVE-2021-44228](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44228). 6 | It does this by injecting actual `Logger.error("${jndi:ldap://...})` calls into the running application and checking for 7 | LDAP network requests made by Log4j. 8 | 9 | Specifically, this confirms vulnerability to data exfiltration _and_ remote code execution (it does not differentiate between those two). 10 | 11 | ## Usage 12 | 13 | (For now you need to compile using Java 11, but you can run it on any version 8 or 11+.) 14 | ```sh 15 | mvn package 16 | ``` 17 | 18 | Java 8: 19 | ```sh 20 | java -cp /lib/tools.jar:target/is-it-vulnerable-1.0-SNAPSHOT.jar com.stripe.log4j.isitvuln.IsItVuln 21 | ``` 22 | 23 | Java 11: 24 | ```sh 25 | java -jar target/is-it-vulnerable-1.0-SNAPSHOT.jar 26 | ``` 27 | 28 | This will attempt to inject code into all JVM's running on the machine, and report status in CSV format. 29 | 30 | ### Example output 31 | 32 | Sample output to `stdout`: 33 | 34 | ```csv 35 | date,host,tool,version,pid,path,jre,log4j,log4j version,formatMsgNoLookups,ldap trustURLCodebase,rmi trustURLCodebase,cosnaming trustURLCodebase,exploited 36 | 2021-12-14T22:58:04.435774Z,st-keithl1,is-it-vuln,1,18742,/Library/Java/JavaVirtualMachines/amazon-corretto-11.jdk/Contents/Home,Amazon.com Inc. - OpenJDK 64-Bit Server VM - 11.0.13,true,false,unknown,unknown,true,none,vulnerable 37 | 2021-12-14T22:58:04.435748Z,st-keithl1,is-it-vuln,1,18741,/Library/Java/JavaVirtualMachines/amazon-corretto-11.jdk/Contents/Home,Amazon.com Inc. - OpenJDK 64-Bit Server VM - 11.0.13,true,unknown,unknown,unknown,unknown,none,not vulnerable 38 | ``` 39 | 40 | | date | host | tool | version | pid | path | jre | log4j | log4j version | formatMsgNoLookups | ldap trustURLCodebase | rmi trustURLCodebase | cosnaming trustURLCodebase | exploited | 41 | | --------------------------- | ---------- | ---------- | ------- | ----- | ---------------------------------------------------------------------- | ---------------------------------------------------- | ----- | ------------- | ------------------ | --------------------- | -------------------- | -------------------------- | -------------- | 42 | | 2021-12-14T22:58:04.435774Z | st-keithl1 | is-it-vuln | 1 | 18742 | /Library/Java/JavaVirtualMachines/amazon-corretto-11.jdk/Contents/Home | Amazon.com Inc. - OpenJDK 64-Bit Server VM - 11.0.13 | true | false | unknown | unknown | true | none | vulnerable | 43 | | 2021-12-14T22:58:04.435748Z | st-keithl1 | is-it-vuln | 1 | 18741 | /Library/Java/JavaVirtualMachines/amazon-corretto-11.jdk/Contents/Home | Amazon.com Inc. - OpenJDK 64-Bit Server VM - 11.0.13 | true | unknown | unknown | unknown | unknown | none | not vulnerable | 44 | 45 | The last column is the most important: it is `vulnerable` or `not vulnerable`. If 46 | `vulnerable` this indicates that Log4J did make an HTTP request when logging a `${jndi:...}` string. 47 | 48 | It also prints status updates to stderr: 49 | 50 | ```sh 51 | Will use this jar for agent: /Users/.../target/is-it-vulnerable-1.0-SNAPSHOT.jar 52 | Attempting to attach to to 23378 running org.jetbrains.idea.maven.server.RemoteMavenServer36 53 | Attempting to attach to to 24730 running org.jetbrains.idea.maven.server.RemoteMavenServer36 54 | Attempting to attach to to 36200 running LDAPRefServer 55 | Attempting to attach to to 18741 running org.jetbrains.jps.cmdline.Launcher 56 | Attempting to attach to to 8383 running 57 | Attempting to attach to to 18742 running com.stripe.Repro 58 | Failed to attach to 24730 running org.jetbrains.idea.maven.server.RemoteMavenServer36: com.sun.tools.attach.AgentInitializationException: Agent JAR loaded but agent failed to initialize 59 | Failed to attach to 23378 running org.jetbrains.idea.maven.server.RemoteMavenServer36: com.sun.tools.attach.AgentInitializationException: Agent JAR loaded but agent failed to initialize 60 | Failed to attach to 8383 running : com.sun.tools.attach.AgentInitializationException: Agent JAR loaded but agent failed to initialize 61 | Failed to attach to 36200 running LDAPRefServer: com.sun.tools.attach.AgentInitializationException: Agent JAR loaded but agent failed to initialize 62 | Parsing /var/folders/zd/71fgr5392y79q54tzgc2zn700000gn/T/is-it-vuln501360083800619055/result-18742.csv 63 | unknown,st-keithl1,full,1,18742,/Library/Java/JavaVirtualMachines/amazon-corretto-11.jdk/Contents/Home,Amazon.com Inc. - OpenJDK 64-Bit Server VM - 11.0.13,true,none,false,unknown,unknown,true,none,vulnerable,maybe 64 | Parsing /var/folders/zd/71fgr5392y79q54tzgc2zn700000gn/T/is-it-vuln501360083800619055/result-18741.csv 65 | 66 | ``` 67 | 68 | In your running application you should see this output (though if you don't, it doesn't invalidate the CSV printed by the tool itself): 69 | 70 | ```sh 71 | Attempting to log IsItVuln ExploitAttempt ${jndi:ldap://127.0.0.1:61092/x} 72 | 17:39:40.239 [Attach Listener] ERROR com.stripe.log4j.isitvuln.IsItVulnAgent - IsItVuln ExploitAttempt ${jndi:ldap://127.0.0.1:61092/x} 73 | ``` 74 | -------------------------------------------------------------------------------- /confirm-vulnerabilities/src/main/java/com/stripe/log4j/isitvuln/InspectedJVM.java: -------------------------------------------------------------------------------- 1 | package com.stripe.log4j.isitvuln; 2 | 3 | import com.sun.tools.attach.AgentInitializationException; 4 | import com.sun.tools.attach.AgentLoadException; 5 | import com.sun.tools.attach.VirtualMachine; 6 | import sun.jvmstat.monitor.MonitoredHost; 7 | import sun.jvmstat.monitor.MonitoredVm; 8 | import sun.jvmstat.monitor.MonitoredVmUtil; 9 | import sun.jvmstat.monitor.VmIdentifier; 10 | 11 | import java.io.FileReader; 12 | import java.io.IOException; 13 | import java.net.ServerSocket; 14 | import java.net.Socket; 15 | import java.nio.file.Path; 16 | import java.time.ZoneOffset; 17 | import java.time.ZonedDateTime; 18 | import java.time.format.DateTimeFormatter; 19 | import java.util.Date; 20 | import java.util.Properties; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.function.Consumer; 23 | 24 | import static com.stripe.log4j.isitvuln.IsItVuln.VERSION_NUMBER; 25 | 26 | class InspectedJVM { 27 | public static final int MIN_WAIT_SECS = 5; 28 | int pid; 29 | MonitoredHost host; 30 | Path output; 31 | 32 | String vmName; 33 | String vendor; 34 | String vmVersion; 35 | String args; 36 | String javaHome; 37 | Exception exception; 38 | String javaVersion; 39 | private Properties sysProps; 40 | 41 | int ldapServerPort = -1; 42 | volatile boolean gotLdapConnection = false; 43 | volatile boolean pleaseFinish = false; 44 | private final Consumer print; 45 | 46 | InspectedJVM(int pid, MonitoredHost host, Path outputFile, Consumer print) { 47 | this.pid = pid; 48 | this.host = host; 49 | this.output = outputFile; 50 | this.print = print; 51 | } 52 | 53 | public void inspect() { 54 | 55 | String mainClass = "unknown"; 56 | try { 57 | MonitoredVm jvm = host.getMonitoredVm(new VmIdentifier(Integer.toString(pid))); 58 | mainClass = MonitoredVmUtil.mainClass(jvm, true); 59 | this.args = MonitoredVmUtil.jvmArgs(jvm); 60 | this.javaHome = (String) jvm.findByName("java.property.java.home").getValue(); 61 | this.vmName = (String) jvm.findByName("java.property.java.vm.name").getValue(); 62 | this.vendor = (String) jvm.findByName("java.property.java.vm.vendor").getValue(); 63 | this.vmVersion = MonitoredVmUtil.vmVersion(jvm); 64 | this.javaVersion = (String) jvm.findByName("java.property.java.version").getValue(); 65 | this.startServer(); 66 | 67 | System.err.println("Attempting to attach to to " + pid + " running " + mainClass); 68 | VirtualMachine vm = VirtualMachine.attach(Integer.toString(pid)); 69 | this.sysProps = vm.getSystemProperties(); 70 | installAgent(this.output.toString(), this.ldapServerPort, vm); 71 | } catch (Exception e) { 72 | this.exception = e; 73 | //TODO: write this to the output csv? 74 | System.err.println("Failed to attach to " + pid + " running " + mainClass + ": " + e); 75 | e.printStackTrace(); 76 | return; 77 | } 78 | 79 | long start = System.nanoTime(); 80 | while (!this.output.toFile().exists() && !(pleaseFinish && secondsElapsed(start) > MIN_WAIT_SECS)) { 81 | try { 82 | Thread.sleep(1000); 83 | } catch (InterruptedException e) { 84 | e.printStackTrace(); 85 | break; 86 | } 87 | } 88 | if (!this.output.toFile().exists()) { 89 | System.err.println("Never saw creation of " + this.output); 90 | } 91 | printCsv(); 92 | } 93 | 94 | private long secondsElapsed(long nanos) { 95 | return TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - nanos); 96 | } 97 | 98 | private static void installAgent(String path, int port, VirtualMachine vm) 99 | throws IOException, AgentLoadException, AgentInitializationException { 100 | //TODO: how to force reload agent? 101 | try { 102 | vm.loadAgent(ProcessInfo.THIS_JAR_PATH, VERSION_NUMBER + " " + port + " " + path); 103 | } catch (AgentLoadException e) { 104 | // dunno why this exception message means success but it does 105 | if (!"0".equals(e.getMessage())) { 106 | throw e; 107 | } 108 | } 109 | } 110 | 111 | private void printCsv() { 112 | System.err.println("Parsing " + this.output); 113 | Properties propsFromAgent; 114 | try (FileReader reader = new FileReader(this.output.toFile())) { 115 | propsFromAgent = new Properties(); 116 | propsFromAgent.load(reader); 117 | 118 | } catch (IOException e) { 119 | System.err.println("Error reading output files"); 120 | e.printStackTrace(); 121 | System.exit(10); 122 | return; 123 | } 124 | 125 | String log4jVersion = propsFromAgent.getProperty("log4j", "error"); 126 | StringBuilder line = new StringBuilder(FileFormats.toCsv( 127 | ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT), 128 | HostInfo.HOSTNAME, 129 | "is-it-vuln", 130 | VERSION_NUMBER, 131 | Integer.toString(this.pid), 132 | this.javaHome, 133 | this.vendor + " - " + this.vmName + " - " + this.javaVersion, 134 | propsFromAgent.getProperty("hasLog4j", "maybe") 135 | )); 136 | for (String key : FileFormats.SYSPROPS_FOR_CSV) { 137 | line.append(","); 138 | line.append(FileFormats.toCsv(sysProps.getProperty(key, "unknown"))); 139 | } 140 | line.append(","); 141 | line.append(FileFormats.toCsv( 142 | log4jVersion, 143 | propsFromAgent.getProperty("exploited", this.gotLdapConnection ? "vulnerable" : "not vulnerable"))); 144 | print.accept(line.toString()); 145 | } 146 | 147 | public void startServer() throws IOException { 148 | ServerSocket ss = new ServerSocket(); 149 | ss.bind(null); 150 | ldapServerPort = ss.getLocalPort(); 151 | Thread t = new Thread(() -> { 152 | try { 153 | Socket a = ss.accept(); 154 | gotLdapConnection = true; 155 | // If we don't close, log4j hangs for a long time! 156 | a.close(); 157 | } catch (IOException e) { 158 | //TODO: log? 159 | e.printStackTrace(); 160 | } 161 | }); 162 | t.setDaemon(true); 163 | t.start(); 164 | } 165 | 166 | public void pleaseFinish() { 167 | pleaseFinish = true; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /find-vulnerabilities/fingerprint.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type log4jFingerprint struct { 4 | file string 5 | sha256 string 6 | version string 7 | } 8 | 9 | var log4jFingerprints = []log4jFingerprint{ 10 | {file: "org/apache/logging/log4j/core/Filter.class", sha256: "28494eae67f5aad31597a7c883ea3a485e59983acbddf8c34931d038ae1880e5", version: "2.15.0"}, 11 | {file: "org/apache/logging/log4j/core/LoggerContext.class", sha256: "b730113a53921e597a68dedd148977787ab1c6f045844868dbaa6e0d89e2e518", version: "2.14.1"}, 12 | {file: "org/apache/logging/log4j/core/LoggerContext.class", sha256: "7875ed21f5b41de8f50df3d6ccf406c1b51432473f9c8e7fd5a6476a42aa13b8", version: "2.14.0"}, 13 | {file: "org/apache/logging/log4j/core/appender/AbstractManager.class", sha256: "e1c1408b4564593d06016e84a17ec43031e10c36e1083d0538305f438d491517", version: "2.13.1"}, 14 | {file: "org/apache/logging/log4j/core/Logger.class", sha256: "8c3ddc94600aa8696077fd67dc6cda00af740c96b047c2cfee0a333e04ef5b60", version: "2.12.1"}, 15 | {file: "org/apache/logging/log4j/core/LoggerContext.class", sha256: "41ef81bc32b51ac9637f313f1140c1898409a23636531811bfc746315b731d16", version: "2.12.0"}, 16 | {file: "org/apache/logging/log4j/core/LoggerContext.class", sha256: "c276ceae01fa13d9ee8f57fd5f4b2861b0ae36e3d16222a0504ee2ed9154c080", version: "2.11.2"}, 17 | {file: "org/apache/logging/log4j/core/appender/AsyncAppender.class", sha256: "b5825fde082dcb6a05f15b3c6f514c9f73f7f8f9b95ec8da9d9a7947da918af4", version: "2.11.1"}, 18 | {file: "org/apache/logging/log4j/core/AbstractLogEvent.class", sha256: "08e44a7a56082e3e65c8b6970fd3bb85cd067264dea6c7e0e6656d086c8967de", version: "2.11.0"}, 19 | {file: "org/apache/logging/log4j/core/appender/ConsoleAppender.class", sha256: "20fbcf52f3fdd2058deedd12ca3c495f7c00caef5a863d8f85a61c9a4b2e8357", version: "2.10.0"}, 20 | {file: "org/apache/logging/log4j/core/appender/AsyncAppender.class", sha256: "64b35cf90cd9d5e3a66643928bffbd7fc8f92e12d115d57a67a72acbcb214a22", version: "2.9.1"}, 21 | {file: "org/apache/logging/log4j/core/appender/mom/JmsManager.class", sha256: "0260be3bb7a6c3bb5fa3792d2827e5bf240a90afb4dcccd72757ce19fa2dfdf0", version: "2.9.0"}, 22 | {file: "org/apache/logging/log4j/core/LoggerContext.class", sha256: "54e97ca71b5250ad5c2b5f915c77281d542e667bfd7faed030f7f737f30bf170", version: "2.8.2"}, 23 | {file: "org/apache/logging/log4j/core/appender/MemoryMappedFileManager.class", sha256: "de63748c77f86891f4909b8a11b3eebc5d4b8559ab42824b81316e0ae5d90c42", version: "2.8.1"}, 24 | {file: "org/apache/logging/log4j/core/LoggerContext.class", sha256: "13a5f4e5de929b9f2506a9f10747729f39294dbca528bb865da216050948c6a4", version: "2.8"}, 25 | {file: "org/apache/logging/log4j/ThreadContextAccess.class", sha256: "c8b3e6cb25b261a796b1fd5d6cd23dbfe1b1ef6d21bd9daf08e0cc77497ab92a", version: "2.7"}, 26 | {file: "org/apache/logging/log4j/core/LoggerContext.class", sha256: "dcda8fb83531e52ab417106640554815d91e0571d2d8b9cd9cc29d3e071dda45", version: "2.6.2"}, 27 | {file: "org/apache/logging/log4j/core/appender/OutputStreamManager.class", sha256: "387abe5220e2d26bf36c1037ebf343d0693ae304f7ad537f8c5f29352c53cd04", version: "2.6.1"}, 28 | {file: "org/apache/logging/log4j/core/appender/OutputStreamManager.class", sha256: "f1c657573cf676bf14f63d102e71d481f32968c139e27359f447a4583eb6e29d", version: "2.6"}, 29 | {file: "org/apache/logging/log4j/core/Logger.class", sha256: "dc6bbc06f212f040146711ad8b271a8d85b135f6504fd0f482086cf00e570da6", version: "2.5"}, 30 | {file: "org/apache/logging/log4j/core/Logger.class", sha256: "fbf8b2f9b621e3cf9cef933403ea5b1b514d172cd7e32fc792d7e9a7c3a4eb2e", version: "2.4.1"}, 31 | {file: "org/apache/logging/log4j/core/Logger.class", sha256: "9451fb5e697e44bb7f57846f6f10283bcbced5c5d00ddfdc102e90f66e3b229e", version: "2.4"}, 32 | {file: "org/apache/logging/log4j/core/Logger.class", sha256: "f356fd80bc98ff8f41becae91f80c52149ecef1289db95b3f7c5cfb33facaa59", version: "2.3"}, 33 | {file: "org/apache/logging/log4j/core/Logger.class", sha256: "9526d450bb5d1997295f5c3cdda79b007c49995f6ff0e412725205942bae36b0", version: "2.2"}, 34 | {file: "org/apache/logging/log4j/core/AbstractLifeCycle.class", sha256: "774541fb268b292c69a72d0476e0738a1bcb6b51dc77d9d82602805b7c670183", version: "2.1"}, 35 | {file: "org/apache/logging/log4j/core/Logger.class", sha256: "53785598c65f0db6ea42f5a6ec608f6628e605e3fe5df958909037f914427e9c", version: "2.0.2"}, 36 | {file: "org/apache/logging/log4j/core/impl/Log4jLogEvent.class", sha256: "9a23e7642fd7b7dca23013d13cda4db91044f793101c2b9e030564da47cdab42", version: "2.0.1"}, 37 | {file: "org/apache/logging/log4j/core/selector/JndiContextSelector.class", sha256: "d80fa3806065ccfcc0d0c678efe5c756ade1f8a5a72df46dcaed97e571fd7639", version: "2.0"}, 38 | {file: "org/apache/logging/log4j/core/LogEventListener.class", sha256: "52f9c2741dc9fa16ca50aabd61d535624f5fefbbd5d1f88e963fd6a2dbc60717", version: "2.0-rc2"}, 39 | {file: "org/apache/logging/log4j/core/Logger.class", sha256: "5ec127f5ab34c867e82b078431c7b54cdce46b1541c846ae4ecdda8ecccf0adf", version: "2.0-rc1"}, 40 | {file: "org/apache/logging/log4j/core/Logger.class", sha256: "0ab344d7f3a244e76786932979eacafae1778cb53d772627c01464224b1007f4", version: "2.0-beta9"}, 41 | {file: "org/apache/logging/log4j/core/appender/ConsoleAppender.class", sha256: "57174e9835ba74b954271375aaa3dacd57b6976c73ae7e34dc500e86e2525433", version: "2.0-beta8"}, 42 | {file: "org/apache/logging/log4j/core/appender/ConsoleAppender.class", sha256: "2152e50ed5ea5e9a821853c81cf884173a8d51989de4507fde50b6eaace6f81d", version: "2.0-beta7"}, 43 | {file: "org/apache/logging/log4j/core/LoggerContext.class", sha256: "bdc993287ad181389488fa255c48c7f5f2a7add441088363ad87b7c05dcf23a2", version: "2.0-beta6"}, 44 | {file: "org/apache/logging/log4j/core/Logger.class", sha256: "2308742242c03027def9d1e6c614fcfe5f5b74673fc9030718654f9aeeb34ef6", version: "2.0-beta5"}, 45 | {file: "org/apache/logging/log4j/core/Logger.class", sha256: "bf65cd0855b9cdc8b644ec956e34b10dad605c86de3fe7d871bc1d3f2be3c7f6", version: "2.0-beta4"}, 46 | {file: "org/apache/logging/log4j/core/Logger.class", sha256: "4be5c0f7ca3e026af331e3a9f65d4b7af7acc56a52c8c261a30a81061696558f", version: "2.0-beta3"}, 47 | {file: "org/apache/logging/log4j/core/Logger.class", sha256: "da6ce58a6c9fbcb7cd7caf4b2f5c539cf5f8e9bd4d55864d90ed4c4de8015a1d", version: "2.0-beta2"}, 48 | {file: "org/apache/logging/log4j/core/LoggerContext.class", sha256: "24e764b716c678c3dfb905ec730aab7278464d1226e80dcfb62ac829b0b1839e", version: "2.0-alpha2"}, 49 | {file: "org/apache/logging/log4j/core/LoggerContext.class", sha256: "2a6831860b3ab7b54165821ecfd62eb258ea382aa8ae9091281a7c85325a2f37", version: "2.0-alpha1"}, 50 | {file: "org/apache/log4j/ConsoleAppender.class", sha256: "d73ce43dac92d151288da89ff6d765f31fc785ba540b0d18620b73d74aac56d9", version: "1.2.17"}, 51 | {file: "org/apache/log4j/LogXF.class", sha256: "eb02dceb9eb42d04126c30828b1259dfdf9f316f070d46de1343ca832e150031", version: "1.2.16"}, 52 | {file: "org/apache/log4j/AsyncAppender.class", sha256: "56d8abb7d76f00f06396fb3ed20aaaa654cd329c5bbb470e087c85cf8f8cd590", version: "1.2.15"}, 53 | {file: "org/apache/log4j/Dispatcher.class", sha256: "f710cd2b3fdaa29fdbe683ed703a6ac41b4af66127c6b027996049fb65766390", version: "1.2.14"}, 54 | {file: "org/apache/log4j/Dispatcher.class", sha256: "8c4a334ca28b5dae7915eb9c5a75ab740c3a198b7f68b8bcdf29220eb2f718b7", version: "1.2.13"}, 55 | {file: "org/apache/log4j/ConsoleAppender.class", sha256: "612b80ce3c3e54d9c64d24395c09a94b109cf3424c2d3a906b3dfc240b0ac718", version: "1.2.12"}, 56 | {file: "org/apache/log4j/ConsoleAppender.class", sha256: "000e92134b4aed946f96502207075c1de608305fa4952d5dcb2dd607ab064e06", version: "1.2.11"}, 57 | {file: "org/apache/log4j/Logger.class", sha256: "96ea23a6b047028a003399a4454359ad61b38832e1c60d24d6d18b698d3891df", version: "1.2.9"}, 58 | {file: "org/apache/log4j/ConsoleAppender.class", sha256: "24765dd3e6559df2d36b1bb2f3c9ab622bb84aab33fe76d5a0a3113b01daf25a", version: "1.2.8"}, 59 | {file: "org/apache/log4j/LogManager.class", sha256: "f78ec34b23ac14035f0034fa3ed00841b2b66d9f8389b265a4aa07f79744106b", version: "1.2.7"}, 60 | {file: "org/apache/log4j/MDC.class", sha256: "fafdf46644b35e408217890dc40a512df2a75f5aa0437e01cfeb84f9be75bb40", version: "1.2.6"}, 61 | {file: "org/apache/log4j/net/SocketNode.class", sha256: "ed5d53deb29f737808521dd6284c2d7a873a59140e702295a80bd0f26988f53a", version: "1.2.5"}, 62 | {file: "org/apache/log4j/Dispatcher.class", sha256: "ea5d8dac5f550597a914f07a43bcff51f934817412554b54d13a945504c91ef3", version: "1.2.4"}, 63 | {file: "org/apache/log4j/ConsoleAppender.class", sha256: "1cfb24a5822a73d089baea464c2bc0bfe16da5b582bf323af8c813818529a3a6", version: "1.1.3"}, 64 | } 65 | -------------------------------------------------------------------------------- /find-vulnerabilities/log4j.go: -------------------------------------------------------------------------------- 1 | // Searches the system for artifacts related to log4j and prints them to stdout 2 | 3 | package main 4 | 5 | import ( 6 | "archive/zip" 7 | "bufio" 8 | "bytes" 9 | "encoding/csv" 10 | "flag" 11 | "fmt" 12 | "io" 13 | "log" 14 | "os" 15 | "os/exec" 16 | "path/filepath" 17 | "sort" 18 | "strings" 19 | "syscall" 20 | 21 | "github.com/hashicorp/go-version" 22 | "github.com/shirou/gopsutil/process" 23 | ) 24 | 25 | var verbose = flag.Bool("verbose", false, "be more verbose") 26 | 27 | func main() { 28 | flag.Parse() 29 | log.SetOutput(os.Stderr) 30 | defer log.Printf("done") 31 | 32 | if syscall.Geteuid() != 0 { 33 | log.Fatal("this tool must be run as root") 34 | } 35 | 36 | hostname, err := os.Hostname() 37 | if err != nil { 38 | hostname = "unknown" 39 | } 40 | 41 | selfHash := hashFile(os.Args[0]) 42 | 43 | procs, err := process.Processes() 44 | if err != nil { 45 | log.Fatalf("error getting processes: %+v", err) 46 | } 47 | 48 | report := makeReport(procs) 49 | 50 | // Sort by the PID 51 | sort.SliceStable(report, func(i, j int) bool { 52 | return report[i].PID < report[j].PID 53 | }) 54 | 55 | // Sort all versions 56 | for _, entry := range report { 57 | sort.SliceStable(entry.JARs, func(i, j int) bool { 58 | j1 := entry.JARs[i] 59 | j2 := entry.JARs[j] 60 | 61 | // If we have both objects, compare them 62 | if j1.VersionObj != nil && j2.VersionObj != nil { 63 | return j1.VersionObj.LessThan(j2.VersionObj) 64 | } 65 | 66 | // Things with valid versions sort before things 67 | // without versions. 68 | if j1.VersionObj == nil && j2.VersionObj != nil { 69 | return true 70 | } else if j1.VersionObj != nil && j2.VersionObj == nil { 71 | return false 72 | } 73 | 74 | // We have no version objects at all; fall back to just 75 | // comparing the raw strings. 76 | return j1.VersionStr < j2.VersionStr 77 | }) 78 | } 79 | 80 | // Columns: 81 | // 1. Hostname 82 | // 2. Tool (lite vs full) 83 | // 3. Tool sha 84 | // 4. Pid 85 | // 5. Java binary location 86 | // 6. Java version & release (e.g. adoptopenjdk-1.8.0u181) 87 | // 7. Value of log4j2.formatMsgNoLookups 88 | // 8. Value of com.sun.jndi.ldap.object.trustURLCodebase 89 | // 9. Value of com.sun.jndi.rmi.object.trustURLCodebase 90 | // 10. Value of com.sun.jndi.cosnaming.object.trustURLCodebase 91 | // 11. Is using log4j? 92 | // 12. Oldest Log4j version found 93 | // 13. Summary: vulnerable: yes/no/maybe 94 | // 14. Oldest vulnerable log4j version found (i.e. >= 2.0.0) 95 | 96 | w := csv.NewWriter(os.Stdout) 97 | defer w.Flush() 98 | 99 | w.Write([]string{ 100 | "hostname", "tool", "tool_sha", "pid", "java_bin_location", 101 | "java_version", "prop1", "prop2", "prop3", "prop4", 102 | "using_log4j", "oldest_log4j_version", "vulnerable", 103 | "oldest_vulnerable_log4j_version", 104 | }) 105 | 106 | for _, entry := range report { 107 | binaryLocation := "unknown" 108 | if s, err := os.Readlink(fmt.Sprintf("/proc/%d/exe", entry.PID)); err == nil { 109 | binaryLocation = s 110 | } 111 | 112 | fields := []string{ 113 | hostname, 114 | "lite", 115 | selfHash, 116 | fmt.Sprint(entry.PID), 117 | binaryLocation, 118 | entry.SystemProperties["java.version"], 119 | entry.SystemProperties["log4j2.formatMsgNoLookups"], 120 | entry.SystemProperties["com.sun.jndi.ldap.object.trustURLCodebase"], 121 | entry.SystemProperties["com.sun.jndi.rmi.object.trustURLCodebase"], 122 | entry.SystemProperties["com.sun.jndi.cosnaming.object.trustURLCodebase"], 123 | fmt.Sprint(entry.UsingLog4j()), 124 | "set-below", // oldest_log4j_version 125 | "set-below", // vulnerable 126 | "set-below", // oldest_vulnerable_log4j_version 127 | } 128 | 129 | // If we have any JAR files, then we're using Log4j 130 | var ( 131 | oldestVersion = "unknown" 132 | oldestVulnerableStr = "unknown" 133 | oldestVulnerableObj *version.Version 134 | ) 135 | if len(entry.JARs) > 0 { 136 | oldestVersion = entry.JARs[0].VersionStr 137 | 138 | // Find the oldest version of log4j that's vulnerable. 139 | // Note that this list is sorted, so we break at the 140 | // loop when we've found one that matches. 141 | for _, jar := range entry.JARs { 142 | if jar.VersionObj == nil { 143 | log.Println("VersionObj == nil") 144 | continue 145 | } 146 | 147 | // If we find a version that's before version 148 | // 2, or after the fixed version, set the 149 | // "oldest vulnerable" option to the empty 150 | // string, since if we find nothing else this 151 | // isn't vulnerable. 152 | if jar.VersionObj.LessThan(version2) { 153 | oldestVulnerableStr = "" 154 | } else if jar.VersionObj.GreaterThanOrEqual(fixedVersion) { 155 | oldestVulnerableStr = "" 156 | } 157 | 158 | // The actual check for "is within the vulnerable range" 159 | if jar.VersionObj.GreaterThanOrEqual(version2) && jar.VersionObj.LessThan(fixedVersion) { 160 | oldestVulnerableStr = jar.VersionStr 161 | oldestVulnerableObj = jar.VersionObj 162 | } 163 | } 164 | } 165 | fields[11] = oldestVersion 166 | fields[12] = checkVulnerable(entry, oldestVulnerableObj) 167 | fields[13] = oldestVulnerableStr 168 | 169 | w.Write(fields) 170 | } 171 | } 172 | 173 | func checkVulnerable(entry ReportEntry, oldestVersion *version.Version) string { 174 | if !entry.UsingLog4j() { 175 | return "no" 176 | } 177 | 178 | // If we have a version that can't be parsed, then we're going to 179 | // report as "maybe vulnerable". 180 | if oldestVersion == nil { 181 | return "maybe" 182 | } 183 | 184 | // If the version is newer than the fixed version, we're good. 185 | if isPatchedVersion(oldestVersion) { 186 | if *verbose { 187 | log.Printf("not vulnerable: version patched") 188 | } 189 | return "no" 190 | } 191 | 192 | // If we're older than the version that allows disabling message 193 | // lookups, then there's no way to make this safe, so we're vulnerable. 194 | if oldestVersion.LessThan(flagVersion) { 195 | if *verbose { 196 | log.Printf("vulnerable: lookups can't be disabled on this version") 197 | } 198 | return "yes" 199 | } 200 | 201 | // If the system property to disable message lookups is set, then we're 202 | // not vulnerable. 203 | if entry.SystemProperties["log4j2.formatMsgNoLookups"] == "true" { 204 | if *verbose { 205 | log.Printf("not vulnerable: formatMsgNoLookups") 206 | } 207 | return "no" 208 | } 209 | 210 | // If the process has the environment variable to disable lookups set, it's also not vulnerable. 211 | if entry.Environ["LOG4J_FORMAT_MSG_NO_LOOKUPS"] == "true" { 212 | if *verbose { 213 | log.Printf("not vulnerable: LOG4J_FORMAT_MSG_NO_LOOKUPS") 214 | } 215 | return "no" 216 | } 217 | 218 | // If we get here, we're vulnerable 219 | return "yes" 220 | } 221 | 222 | func makeReport(procs []*process.Process) (ret []ReportEntry) { 223 | for _, proc := range procs { 224 | name, err := proc.Name() 225 | if err != nil { 226 | log.Printf("error getting name for process pid=%d", proc.Pid) 227 | name = "unknown" 228 | } 229 | 230 | processPath := "unknown" 231 | if s, err := os.Readlink(fmt.Sprintf("/proc/%d/exe", proc.Pid)); err == nil { 232 | processPath = s 233 | } 234 | 235 | if processPath == "unknown" { 236 | // probably kernel process; skip 237 | continue 238 | } else if name == "unknown" { 239 | // check; don't want to miss this if it's important 240 | } else if strings.Contains(name, "java") { 241 | // check 242 | } else if strings.Contains(processPath, "java") { 243 | // check 244 | } else if strings.Contains(processPath, "/jdk/") || strings.Contains(processPath, "/jre/") || strings.Contains(processPath, "/jvm/") { 245 | // check 246 | } else { 247 | // probably not java 248 | continue 249 | } 250 | 251 | entry := ReportEntry{ 252 | PID: proc.Pid, 253 | ProcName: name, 254 | Environ: make(map[string]string), 255 | SystemProperties: make(map[string]string), 256 | } 257 | if props, err := getSysprops(proc); err == nil { 258 | //log.Printf("sysprops[%d] = %+v", proc.Pid, props) 259 | entry.SystemProperties = props 260 | } else { 261 | if err := checkCommandline(proc, &entry); err != nil { 262 | log.Printf("%v", err) 263 | } 264 | } 265 | if err := checkOpenFiles(proc, &entry); err != nil { 266 | log.Printf("%v", err) 267 | } 268 | if env, err := proc.Environ(); err == nil { 269 | for _, s := range env { 270 | parts := strings.SplitN(s, "=", 2) 271 | if len(parts) == 2 { 272 | entry.Environ[parts[0]] = parts[1] 273 | } 274 | } 275 | } 276 | ret = append(ret, entry) 277 | } 278 | 279 | return 280 | } 281 | 282 | func checkOpenFiles(proc *process.Process, entry *ReportEntry) error { 283 | files, err := proc.OpenFiles() 284 | if err != nil { 285 | return fmt.Errorf("error getting open files for process %q (pid: %d): %w", entry.ProcName, proc.Pid, err) 286 | } 287 | 288 | for _, file := range files { 289 | if strings.Contains(file.Path, "log4j") { 290 | path := pathForFile(proc, file) 291 | 292 | versionStr := "unknown" 293 | versionObj := (*version.Version)(nil) 294 | zr, err := zip.OpenReader(path) 295 | if err == nil { 296 | versionStr = versionFromJARArchive(&zr.Reader) 297 | zr.Close() 298 | 299 | if vo, err := parseVersion(versionStr); err == nil { 300 | versionObj = vo 301 | } 302 | } else if *verbose { 303 | log.Printf("could not open file %q: %v", path, err) 304 | } 305 | 306 | hash := hashFile(path) 307 | entry.JARs = append(entry.JARs, JAREntry{ 308 | Path: file.Path, 309 | VersionStr: versionStr, 310 | VersionObj: versionObj, 311 | SHA256: hash, 312 | }) 313 | } 314 | 315 | if strings.HasSuffix(strings.ToLower(file.Path), ".jar") { 316 | if err := checkJarFile(proc, entry, file); err != nil { 317 | return fmt.Errorf("error checking JAR file %q for process %q (pid: %d): %w", file.Path, entry.ProcName, proc.Pid, err) 318 | } 319 | } 320 | } 321 | 322 | return nil 323 | } 324 | 325 | func checkCommandline(proc *process.Process, entry *ReportEntry) error { 326 | cmdline, err := proc.CmdlineSlice() 327 | if err != nil { 328 | return fmt.Errorf("error getting command line for process %q (pid: %d): %w", entry.ProcName, proc.Pid, err) 329 | } 330 | 331 | for _, part := range cmdline { 332 | if strings.HasPrefix(part, "-D") { 333 | parts := strings.SplitN(part[2:], "=", 2) 334 | if len(parts) == 2 { 335 | entry.SystemProperties[parts[0]] = parts[1] 336 | } 337 | } 338 | } 339 | 340 | return nil 341 | } 342 | 343 | func checkJarFile(proc *process.Process, entry *ReportEntry, openFile process.OpenFilesStat) error { 344 | f, err := os.Open(pathForFile(proc, openFile)) 345 | if err != nil { 346 | return fmt.Errorf("error opening file %q: %w", openFile.Path, err) 347 | } 348 | defer f.Close() 349 | 350 | fi, err := f.Stat() 351 | if err != nil { 352 | return fmt.Errorf("error calling Stat() on file %q: %w", openFile.Path, err) 353 | } 354 | 355 | // Ignore things that aren't files 356 | if !fi.Mode().IsRegular() { 357 | return nil 358 | } 359 | 360 | r, err := zip.NewReader(f, fi.Size()) 361 | if err != nil { 362 | return fmt.Errorf("error opening file %q: %w", openFile.Path, err) 363 | } 364 | 365 | // Check for files matching the word "log4j" in them 366 | for _, f := range r.File { 367 | if strings.Contains(f.Name, "log4j") { 368 | hash := "error" 369 | if fr, err := r.Open(f.Name); err == nil { 370 | hash = hashFsFile(fr) 371 | fr.Close() 372 | } 373 | 374 | entry.Log4JFiles = append(entry.Log4JFiles, FileEntry{ 375 | ContainedIn: openFile.Path, 376 | Path: f.Name, 377 | SHA256: hash, 378 | }) 379 | } 380 | } 381 | 382 | // Also, if this is a deploy/uber-jar, we may have log4j in it even if 383 | // the file name doesn't contain that; try to get that version as well. 384 | if ver := versionFromJARArchiveFingerprint(r); ver != "unknown" { 385 | var versionObj *version.Version 386 | if vo, err := parseVersion(ver); err == nil { 387 | versionObj = vo 388 | } else { 389 | log.Printf("error parsing %q: %v", ver, err) 390 | } 391 | 392 | entry.JARs = append(entry.JARs, JAREntry{ 393 | Path: openFile.Path, 394 | VersionStr: ver, 395 | VersionObj: versionObj, 396 | // TODO: SHA256: hash, 397 | }) 398 | } 399 | return nil 400 | } 401 | 402 | func pathForFile(proc *process.Process, openFile process.OpenFilesStat) string { 403 | return fmt.Sprintf("/proc/%d/fd/%d", proc.Pid, openFile.Fd) 404 | } 405 | 406 | func getSysprops(proc *process.Process) (map[string]string, error) { 407 | // Use the jinfo from "next" to the java process, if it exists 408 | jinfoPath := "/usr/bin/jinfo" 409 | if s, err := os.Readlink(fmt.Sprintf("/proc/%d/exe", proc.Pid)); err == nil { 410 | tpath := filepath.Join(filepath.Dir(s), "jinfo") 411 | if fileExists(tpath) { 412 | jinfoPath = tpath 413 | } 414 | } 415 | if !fileExists(jinfoPath) { 416 | return nil, fmt.Errorf("no jinfo found") 417 | } 418 | 419 | var stdout bytes.Buffer 420 | cmd := exec.Command(jinfoPath, fmt.Sprint(proc.Pid)) 421 | cmd.Stdout = &stdout 422 | cmd.Stderr = io.Discard 423 | 424 | if err := cmd.Run(); err != nil { 425 | return nil, err 426 | } 427 | 428 | ret := make(map[string]string) 429 | scanner := bufio.NewScanner(&stdout) 430 | for scanner.Scan() { 431 | parts := strings.SplitN(scanner.Text(), "=", 2) 432 | if len(parts) == 2 { 433 | ret[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) 434 | } 435 | } 436 | if err := scanner.Err(); err != nil { 437 | return nil, err 438 | } 439 | 440 | return ret, nil 441 | } 442 | 443 | type ReportEntry struct { 444 | PID int32 445 | ProcName string 446 | 447 | // Environment variables 448 | Environ map[string]string 449 | 450 | // List of system properties set for the process 451 | SystemProperties map[string]string 452 | 453 | // JAR files 454 | JARs []JAREntry 455 | 456 | // Log4J-related files in a JAR file 457 | Log4JFiles []FileEntry 458 | } 459 | 460 | type JAREntry struct { 461 | Path string 462 | VersionStr string 463 | VersionObj *version.Version 464 | SHA256 string 465 | } 466 | 467 | type FileEntry struct { 468 | Path string 469 | ContainedIn string // optional 470 | SHA256 string 471 | } 472 | 473 | // Returns the property values in a consistently-sorted format 474 | func (r ReportEntry) PropertyValues() []string { 475 | vs := make([]string, 0, len(r.SystemProperties)) 476 | for _, key := range r.PropertyNames() { 477 | vs = append(vs, r.SystemProperties[key]) 478 | } 479 | return vs 480 | } 481 | 482 | // Returns the property names in a consistently-sorted format. 483 | func (r ReportEntry) PropertyNames() []string { 484 | ks := make([]string, 0, len(r.SystemProperties)) 485 | for k := range r.SystemProperties { 486 | ks = append(ks, k) 487 | } 488 | 489 | sort.Strings(ks) 490 | return ks 491 | } 492 | 493 | func (r ReportEntry) UsingLog4j() bool { 494 | return len(r.JARs) > 0 || len(r.Log4JFiles) > 0 495 | } 496 | --------------------------------------------------------------------------------