OpticFusion1/MCAntiMalware

Make this a separate check

github-actions opened this issue · 0 comments

Make this a separate check

package optic_fusion1.antimalware.scanner.file;

import optic_fusion1.antimalware.AntiMalware;
import optic_fusion1.antimalware.CommandLineParser;
import optic_fusion1.antimalware.check.CacheContainer;
import optic_fusion1.antimalware.check.CheckManager;
import optic_fusion1.antimalware.check.CheckResult;
import optic_fusion1.antimalware.database.Database;
import optic_fusion1.antimalware.scanner.ScanHelper;
import optic_fusion1.antimalware.scanner.Scanner;
import org.apache.commons.codec.digest.DigestUtils;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipFile;

import static optic_fusion1.antimalware.AntiMalware.LOGGER;
import static optic_fusion1.antimalware.utils.I18n.tl;
import static optic_fusion1.antimalware.utils.Utils.fileSystemForZip;
import static optic_fusion1.antimalware.utils.Utils.validClassPath;

/**
 * @author IkeVoodoo
 * */
public class FileScanner {

    private static final Path ANTI_MALWARE_PATH;

    static {
        try {
            ANTI_MALWARE_PATH = new File(AntiMalware.class.getProtectionDomain().getCodeSource().getLocation().toURI()).toPath();
        } catch (URISyntaxException e) {
            throw new IllegalStateException(e);
        }
    }

    private static final String SPIGOT_PLATFORM = "Spigot";

    private final CacheContainer cache;
    private final Database database;
    private final CommandLineParser commandLineParser;
    private final CheckManager checkManager;
    private final BiConsumer<Path, CheckResult> notifications;

    public FileScanner(CacheContainer cache, Database database, CommandLineParser commandLineParser, CheckManager checkManager, BiConsumer<Path, CheckResult> notifications) {
        this.cache = cache;
        this.database = database;
        this.commandLineParser = commandLineParser;
        this.checkManager = checkManager;
        this.notifications = notifications;
    }

    public void scanFile(Path file) {
        // Not needed because this method shouldn't be called with a directory like tf it's FileScanner.scanFile like bruh
//        if (Files.isDirectory(file)) {
//            scanDirectory(file);
//            return;
//        }

        if (ScanHelper.isFileEmpty(file)) {
            return;
        }

        String fileName = file.getFileName().toString();

        this.checkBlacklistedFileName(file, fileName);
        this.checkBlacklistedFilePath(file);

        if (fileName.equals("VaultLib.jar")) {
            this.submitNotification(file.toAbsolutePath(), new CheckResult(SPIGOT_PLATFORM, "MALWARE", "Qlutch", "C"));
        }

        if (!fileName.endsWith(".jar") && !fileName.endsWith(".zip") && !fileName.endsWith(".rar") && !isPlugin(file)) {
            return;
        }

        this.checkFileHash(file);

        try (var fs = fileSystemForZip(file)) {
            if (fs == null) {
                return;
            }

            Path rootFolder = fs.getRootDirectories().iterator().next();
            if (this.commandLineParser.shouldScanZippedFiles() && (fileName.endsWith(".zip") || fileName.endsWith(".rar"))) {
                this.scanZip(file);
                return;
            }

            Scanner.WhitelistResult result = isFileWhitelisted(file);
            if (result == Scanner.WhitelistResult.INVALID_FILE || result == Scanner.WhitelistResult.WHITELISTED) {
                return;
            }

            //region KillMe
            // TODO: Make this a separate check
            if (Files.exists(rootFolder.resolve("dev/jnic/lib/"))) {
                this.submitNotification(file, new CheckResult(SPIGOT_PLATFORM, "Malware", "JNIC"));
//                return;
            }

            // TODO: Make these a separate check
            if (Files.exists(rootFolder.resolve("plugin-config.bin"))) {
                this.submitNotification(file, new CheckResult(SPIGOT_PLATFORM, "Malware", "SG"));
//                return;
            }

            if (Files.exists(rootFolder.resolve("META-INF/gradle/org/apache/commons/local-info.hdm"))) {
                this.submitNotification(file, new CheckResult(SPIGOT_PLATFORM, "Malware", "SG", "B"));
//                return;
            }

            if (Files.exists(rootFolder.resolve("META-INF/gradle/io/netty/netty-locals.netd"))) {
                this.submitNotification(file, new CheckResult(SPIGOT_PLATFORM, "Malware", "SG", "C"));
//                return;
            }

            if (Files.exists(rootFolder.resolve("META-INF/maven/org/apache/logging/log4j/Log4j-events.dtd"))) {
                this.submitNotification(file, new CheckResult(SPIGOT_PLATFORM, "Malware", "SG", "D"));
//                return;
            }

            if (Files.exists(rootFolder.resolve("META-INF/gradle/org/apache/logging/log4j/Log4j-events.dtd"))) {
                this.submitNotification(file, new CheckResult(SPIGOT_PLATFORM, "Malware", "SG", "E"));
//                return;
            }

            if (Files.exists(rootFolder.resolve("META-INF/gradle/org.json/json/json.xsd"))) {
                this.submitNotification(file, new CheckResult(SPIGOT_PLATFORM, "Malware", "SG", "F"));
//                return;
            }

            if (Files.exists(rootFolder.resolve("META-INF/maven/org/apache/commons/api-catch.dir"))) {
                this.submitNotification(file, new CheckResult(SPIGOT_PLATFORM, "Malware", "SG", "G"));
//                return;
            }

            if (Files.exists(rootFolder.resolve("META-INF/maven/org/apache/commons/local-dir.hum"))) {
                this.submitNotification(file, new CheckResult(SPIGOT_PLATFORM, "Malware", "SG", "H"));
//                return;
            }

            if (Files.exists(rootFolder.resolve("META-INF/maven/org/apache/commons/local-info.hdm"))) {
                this.submitNotification(file, new CheckResult(SPIGOT_PLATFORM, "Malware", "SG", "I"));
//                return;
            }

            if (Files.exists(rootFolder.resolve("META-INF/maven/com/google/code/gson/gson/maven.data"))) {
                this.submitNotification(file, new CheckResult(SPIGOT_PLATFORM, "Malware", "SG", "J"));
//                return;
            }

            if (Files.exists(rootFolder.resolve("META-INF/gradle/com.google.code.gson/gson/maven.data"))) {
                this.submitNotification(file, new CheckResult(SPIGOT_PLATFORM, "Malware", "SG", "K"));
//                return;
            }

            if (Files.exists(rootFolder.resolve("META-INF/maven/org.json/json/gson.xsd"))) {
                this.submitNotification(file, new CheckResult(SPIGOT_PLATFORM, "Malware", "SG", "L"));
//                return;
            }
            //endregion

            AtomicBoolean possiblyMalicious = new AtomicBoolean(false);

            walkThroughFiles(rootFolder, classPath -> {
                if (!validClassPath(classPath)) {
                    return;
                }

                var classNode = this.cache.fetchClass(file, classPath);
                if (classNode == null) {
                    return;
                }

                for (var check : this.checkManager.getChecks()) {
                    var results = check.process(classNode, rootFolder, file, cache);
                    if (results == null || results.isEmpty()) continue;

                    possiblyMalicious.set(true);

                    for (var checkResult : results) {
                        if (this.commandLineParser.dontLogINFOCR() && checkResult.getType().equals("INFO")) {
                            continue;
                        }

                        this.submitNotification(file, checkResult);
                    }

                    check.reset();
                }
            });

            this.cache.clearCache(file); // Attempt at fixing memory issues
            if (this.commandLineParser.shouldPrintNotInfectedMessages() && !possiblyMalicious.get()) {
                LOGGER.info(tl("scanner_probably_safe", file));
            }
        } catch (IOException ex) {
            LOGGER.exception(ex);
        }
    }

    private void checkBlacklistedFileName(Path file, String fileName) {
        if (!this.cache.containsBlacklistedFileName(fileName)) {
            return;
        }

        try {
            var result = this.database.getCheckResultForFileName(fileName);
            this.submitNotification(file.toAbsolutePath(), result);
        } catch (SQLException ex) {
            LOGGER.exception(ex);
        }
    }

    private void checkBlacklistedFilePath(Path file) {
        if (!this.cache.containsBlacklistedFilePath(file.toString())) {
            return;
        }

        try {
            var result = this.database.getCheckResultForFilePath(file.toString());
            this.submitNotification(file.toAbsolutePath(), result);
        } catch (SQLException ex) {
            LOGGER.exception(ex);
        }
    }

    private void checkFileHash(Path file) {
        var checksum = this.cache.fetchSHA1(file, file);

        if (checksum == null) {
            LOGGER.warn("The SHA-1 checksum for '" + file + "' couldn't be loaded");
            return;
        }

        checksum = checksum.toUpperCase();
        if (!this.cache.containsBlacklistedChecksum(checksum)) {
            return;
        }

        try {
            var checkResult = this.database.getCheckResultForChecksum(checksum);
            if (checkResult == null) {
                LOGGER.info(tl("scanner_blacklisted_not_in_database", file)); // Sysout replaced with logger
                return;
            }

            if (this.commandLineParser.dontLogINFOCR() && checkResult.getType().equals("INFO")) {
                return;
            }

            this.submitNotification(file, checkResult);
        } catch (SQLException ex) {
            LOGGER.exception(ex);
        }
    }

    private void submitNotification(Path path, CheckResult result) {
        if (result == null) return;

        this.notifications.accept(path, result);
    }

    private boolean isPlugin(Path file) {
        try(var zipFile = new ZipFile(file.toFile())) {
            return zipFile.getEntry("plugin.yml") != null;
        } catch (IOException ex) {
            return false;
        }
    }

    private Scanner.WhitelistResult isFileWhitelisted(Path file) {
        if (file == null || ScanHelper.isFileEmpty(file)) {
            return Scanner.WhitelistResult.INVALID_FILE;
        }

        try {
            if (Files.isSameFile(file, ANTI_MALWARE_PATH)) {
                return Scanner.WhitelistResult.WHITELISTED;
            }

            var fileChecksum = DigestUtils.sha1Hex(Files.newInputStream(file));
            var result = isChecksumWhitelisted(fileChecksum);

            if (result == Scanner.WhitelistResult.WHITELISTED && this.commandLineParser.shouldPrintNotInfectedMessages()) {
                LOGGER.info(tl("scanner_probably_safe_whitelisted", file.getFileName().toString()));
            }

            return result;
        } catch (IOException e) {
            LOGGER.exception(e);
        }

        return Scanner.WhitelistResult.NOT_WHITELISTED;
    }

    protected void walkThroughFiles(Path dir, Consumer<Path> pathConsumer) {
        if (".".equals(String.valueOf(dir.getFileName()))) { // if (dir.getFileName() != null && dir.getFileName().toString().equals(".")) {
            return;
        }

        if (Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS)) {
            this.walkTroughDirectory(dir, pathConsumer);
            return;
        }

        if (Files.isSymbolicLink(dir)) {
            return;
        }

        pathConsumer.accept(dir);
    }

    private void walkTroughDirectory(Path dir, Consumer<Path> pathConsumer) {
        var stack = new LinkedList<Path>();
        stack.add(dir);

        while (!stack.isEmpty()) {
            var current = stack.pollLast();

            try(var list = Files.list(current).filter(path -> !Files.isSymbolicLink(path))) {
                var iterator = list.iterator();

                while (iterator.hasNext()) {
                    var next = iterator.next();
                    pathConsumer.accept(next);

                    if (Files.isDirectory(next, LinkOption.NOFOLLOW_LINKS)) {
                        stack.addLast(next);
                    }
                }
            } catch (IOException e) {
                LOGGER.exception(e);
            }
        }
    }

    private void scanZip(Path zippedFile) {
        LOGGER.info("Scanning zip file " + zippedFile);

        try(var walk = Files.walk(zippedFile)) {
            walk.forEach(found -> {
                if (Files.isDirectory(found)) {
                    // Scan dir
                    this.scanDirectory(found);
                    return;
                }

                // Scan file
                this.scanFile(found);
            });
        } catch (IOException ex) {
            Logger.getLogger(Scanner.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void scanDirectory(Path directory) {
        try(var list = Files.list(directory)) {
            list.forEach((path) -> {
                if (!path.getFileSystem().isOpen()) return;

                if (Files.isDirectory(path)) {
                    scanDirectory(path);
                    return;
                }

                this.scanFile(path);
            });
        } catch (IOException e) {
            LOGGER.exception(e);
        }
    }

    private Scanner.WhitelistResult isChecksumWhitelisted(String checksum) {
        return this.cache.containsWhitelistedChecksum(checksum) ? Scanner.WhitelistResult.WHITELISTED : Scanner.WhitelistResult.NOT_WHITELISTED;
    }

}

96db68db35bb5a35c1ade12ee18329db31baac75