/* SAAF: A static analyzer for APK files. * Copyright (C) 2013 syssec.rub.de * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package de.rub.syssec.saaf.misc.config; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.HashSet; import java.util.Properties; import org.apache.log4j.FileAppender; import org.apache.log4j.Logger; import org.apache.log4j.PatternLayout; import org.apache.log4j.varia.FallbackErrorHandler; import de.rub.syssec.saaf.db.datasources.Datasource; import de.rub.syssec.saaf.db.datasources.XMLBTPatternSource; import de.rub.syssec.saaf.db.datasources.XMLHPatternSource; import de.rub.syssec.saaf.db.datasources.XMLPermissionDatasource; import de.rub.syssec.saaf.db.persistence.exceptions.PersistenceException; import de.rub.syssec.saaf.db.persistence.interfaces.EntityManagerFacade; import de.rub.syssec.saaf.db.persistence.nodb.NoDBEntityManager; import de.rub.syssec.saaf.db.persistence.sql.EntityManagerImpl; import de.rub.syssec.saaf.misc.adchecker.AdChecker; import de.rub.syssec.saaf.misc.adchecker.AdNetwork; import de.rub.syssec.saaf.misc.adchecker.NameComparingAdChecker; import de.rub.syssec.saaf.misc.adchecker.XMLAdnetworkDataSource; import de.rub.syssec.saaf.model.analysis.BTPatternInterface; import de.rub.syssec.saaf.model.analysis.HPatternInterface; import de.rub.syssec.saaf.model.application.PermissionInterface; /** * The goto-place to obtain information from the config file * * @author Johannes Hoffmann <johannes.hoffmann@rub.de> * @author Tilman Bender <tilman.bender@rub.de> * @author Hanno Lemoine <hanno.lemoine@gdata.de> * */ public class Config implements ConfigInterface { private static final String APP_DIRECTORY = "path_apps"; private static final String BYTECODE_DIRECTORY = "path_bytecode"; // key value store taht is read from the config file // and modified by cmdline parameters private Properties settings; private static Logger LOGGER; private static Config instance; public synchronized static Config getInstance() { if (instance == null) { instance = new Config(); } return instance; } private Config() { try { LOGGER = Logger.getLogger(Config.class); readConfigFile(); validate(); } catch (Exception e) { System.out .println("There was a problem when parsing the config file 'saaf.conf'.\n" + "Please make sure you have a valid saaf.conf in the current directory\n" + "Alternatively set the environment variable SAAF_HOME to tell SAAF where to look for its files.\n" + "A sample configuration file can be found in folder doc-manually"); System.exit(1); } } /** * @throws FileNotFoundException * @throws IOException */ public void readConfigFile() throws FileNotFoundException, IOException { // check if the SAAF_HOME environment variable settings = new Properties(); String saafHome = System.getenv("SAAF_HOME"); if (saafHome != null) { setConfigValue(ConfigKeys.DIRECTORY_HOME, saafHome); } else { // default to local directory if the variable is not set setConfigValue(ConfigKeys.DIRECTORY_HOME, System.getProperty("user.dir")); } LOGGER.info("SAAF will be looking for configuration in:" + getConfigValue(ConfigKeys.DIRECTORY_HOME) + File.separator + "conf"); File config = new File(getConfigValue(ConfigKeys.DIRECTORY_HOME) + File.separator + "conf" + File.separator + "saaf.conf"); FileInputStream propInputStream = null; if (config.exists()) { propInputStream = new FileInputStream(config); } else { propInputStream = new FileInputStream( getConfigValue(ConfigKeys.DIRECTORY_HOME) + File.separator + "conf" + File.separator + "saaf.conf"); } settings.load(propInputStream); propInputStream.close(); } /** * Perform several checks on the values specified in the configuration. * * Checks include: * <ul> * <li>existence of executables * <li>permissions to execute executables * <li>check if directories exist and are readable * </ul> */ public void validate() { boolean foundErrors = false; HashSet<Object> keysInConfigFile = new HashSet<Object>( settings.keySet()); LOGGER.info("Validating configuration..."); for (Object keyInConfigFile : keysInConfigFile) { String entry = (String) keyInConfigFile; try { if (entry.startsWith("path_")) { File f = new File(settings.getProperty(entry)); if (!f.exists() && (entry.equalsIgnoreCase(APP_DIRECTORY) || entry .equalsIgnoreCase(BYTECODE_DIRECTORY))) f.mkdirs(); if (!f.exists() || !f.isDirectory() || !f.canRead()) { LOGGER.error(entry + "=" + settings.getProperty(entry) + ": Directory does not exist or is not readable!"); foundErrors = true; } else { // System.out.println("CONF: "+entry+"="+prop.getProperty(entry)); } } } catch (IllegalArgumentException e) { LOGGER.warn("Problem validating config: " + e.getMessage()); } } foundErrors = hasMissingExecutable(); if (!settings.containsKey(ConfigKeys.DIRECTORY_APPS.toString())) { LOGGER.error("No 'apps' directory configured!"); foundErrors = true; } if (!settings.containsKey(ConfigKeys.DIRECTORY_BYTECODE.toString())) { LOGGER.error("No 'bytecode' directory configured!"); foundErrors = true; } if (!foundErrors) { LOGGER.info("Config looks sane, proceeding."); } else { LOGGER.error("Found errors in the configuration, aborting."); System.exit(1); } } /** * Checks if the required external programss (for graphing, decompiling * etc.) are required and available. * * @return */ private boolean hasMissingExecutable() { boolean foundErrors = false; // check if the external executables are required and available if (getBooleanConfigValue(ConfigKeys.ANALYSIS_GENERATE_JAVA)) { foundErrors |= !isValidExecutable(ConfigKeys.EXECUTABLE_JAD); } if (getBooleanConfigValue(ConfigKeys.ANALYSIS_GENERATE_FUZZYHASH)) { foundErrors |= !isValidExecutable(ConfigKeys.EXECUTABLE_SSDEEP); } return foundErrors; } public boolean isValidExecutable(ConfigKeys key) { boolean isValid = true; String keyName = key.toString(); if (settings.containsKey(keyName)) { File f = new File(this.settings.getProperty(keyName)); if (!f.exists()) { LOGGER.warn(key + "=" + settings.getProperty(keyName) + ": File does not exist!"); isValid = false; } else if (!f.canExecute()) { LOGGER.error(key + "=" + settings.getProperty(keyName) + ": File is not executable!"); isValid = false; } }else{ LOGGER.error(key + " could not be found in the configuration"); isValid=false; } return isValid; } public AdChecker getAdChecker() { String adnetworks = this .getConfigValue(ConfigKeys.DATASOURCE_AD_NETWORKS); String schema = this .getConfigValue(ConfigKeys.DATASOURCE_SCHEMA_AD_NETWORKS); Datasource<AdNetwork> ds = new XMLAdnetworkDataSource(adnetworks, schema); return NameComparingAdChecker.getInstance(ds); } /** * Sets up an additional log appender that writes logging output to an * apk-specific logfile (separate from the one configured in * log4j.properties). * * This was introduced in #36 to allow users to have a per-analysis logfile * instead of logging everything to one global saaf.log * * @param filename * relative or absolute path to logfile. Intermediate directories * will be created automatically. */ public void setupAnalysisLogfile(String filename) { PatternLayout layout = new PatternLayout( "%d{dd MMM yyyy HH:mm:ss,SSS} [%t] %-5p %C{1} %x - %m%n"); if (this.getBooleanConfigValue(ConfigKeys.LOGGING_CREATE_SEPERATE)) { FileAppender appender; try { String path = (getConfigValue(ConfigKeys.LOGGING_FILE_PATH) != null) ? getConfigValue(ConfigKeys.LOGGING_FILE_PATH) : filename; appender = new FileAppender(layout, path); appender.setErrorHandler(new FallbackErrorHandler()); appender.setName("APK Analysis Appender"); org.apache.log4j.Logger.getRootLogger().addAppender(appender); } catch (IOException e) { org.apache.log4j.Logger.getLogger(Config.class).warn( "Failed to create a dedicated log " + filename, e); } } } public Datasource<HPatternInterface> getHTPatternSource() { String patterns = this .getConfigValue(ConfigKeys.DATASOURCE_HEURISTIC_PATTERNS); String schema = this .getConfigValue(ConfigKeys.DATASOURCE_SCHEMA_HEURISTIC_PATTERNS); return new XMLHPatternSource(patterns, schema); } public Datasource<BTPatternInterface> getBTPatternSource() { String patterns = this .getConfigValue(ConfigKeys.DATASOURCE_SLICING_PATTERNS); String schema = this .getConfigValue(ConfigKeys.DATASOURCE_SCHEMA_SLICING); return new XMLBTPatternSource(patterns, schema); } @Override public String getValue(ConfigKeys key) { return getConfigValue(key, key.defaultString); } @Override public void setConfigValue(ConfigKeys key, String value) { settings.setProperty(key.toString(), String.valueOf(value)); } @Override public String getConfigValue(ConfigKeys key, String defaultValue) { return settings.getProperty(key.toString(), defaultValue); } @Override public int getIntConfigValue(ConfigKeys key, int defaultValue) { String r = settings.getProperty(key.toString()); int ret = defaultValue; if (r != null) { ret = Integer.parseInt(r); } return ret; } @Override public void setIntConfigValue(ConfigKeys key, int value) { settings.setProperty(key.toString(), String.valueOf(value)); } @Override public void setBooleanConfigValue(ConfigKeys key, boolean value) { this.setBooleanConfigValue(key.toString(), value); } public Datasource<PermissionInterface> getPermissionSource() { String patterns = this .getConfigValue(ConfigKeys.DATASOURCE_PERMISSIONS); String schema = this .getConfigValue(ConfigKeys.DATASOURCE_SCHEMA_PERMISSIONS); return new XMLPermissionDatasource(patterns, schema); } public EntityManagerFacade getEntityManager() throws PersistenceException { EntityManagerFacade facade = new NoDBEntityManager(); if (!this.getBooleanConfigValue(ConfigKeys.DATABASE_DISABLED)) { facade = new EntityManagerImpl(this); } return facade; } public void setBooleanConfigValue(String key, boolean value) { settings.setProperty(key, String.valueOf(value)); } public boolean getBooleanConfigValue(ConfigKeys key, boolean defaultValue) { String r = settings.getProperty(key.toString()); boolean ret = defaultValue; if (r != null) { ret = Boolean.parseBoolean(r); } return ret; } public boolean getBooleanConfigValue(ConfigKeys key) { return this.getBooleanConfigValue(key, key.defaultBoolean); } public String getConfigValue(ConfigKeys key) { return this.getConfigValue(key, key.defaultString); } public File getFileConfigValue(ConfigKeys key) { return new File(settings.getProperty(key.toString(), key.defaultString)); } }