use std::{
cmp::{Ordering, PartialOrd},
collections::{btree_set::Iter, BTreeSet},
convert::From,
fs,
path::{Path, PathBuf},
slice::Iter as VecIter,
str::FromStr,
usize,
};
use clap::ArgMatches;
use colored::Colorize;
use failure::{format_err, Error, ResultExt};
use num_cpus;
use serde::{de, Deserialize, Deserializer};
use toml::{self, value::Value};
use crate::{criticality::Criticality, print_warning, static_analysis::manifest};
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct Config {
app_packages: Vec<PathBuf>,
verbose: bool,
quiet: bool,
overall_force: bool,
force: bool,
bench: bool,
open: bool,
json: bool,
html: bool,
min_criticality: Criticality,
threads: usize,
downloads_folder: PathBuf,
dist_folder: PathBuf,
results_folder: PathBuf,
dex2jar_folder: PathBuf,
jd_cmd_file: PathBuf,
rules_json: PathBuf,
templates_folder: PathBuf,
template: String,
#[serde(deserialize_with = "ConfigDeserializer::deserialize_unknown_permission")]
unknown_permission: (Criticality, String),
permissions: BTreeSet<Permission>,
loaded_files: Vec<PathBuf>,
}
struct ConfigDeserializer;
type CriticalityString = (Criticality, String);
impl ConfigDeserializer {
pub fn deserialize_unknown_permission<'de, D>(de: D) -> Result<CriticalityString, D::Error>
where
D: Deserializer<'de>,
{
let deserialize_result: Value = Deserialize::deserialize(de)?;
#[allow(clippy::use_debug)]
match deserialize_result {
Value::Table(ref table) => {
let criticality_str = table
.get("criticality")
.and_then(Value::as_str)
.ok_or_else(|| {
de::Error::custom("criticality field not found for unknown permission")
})?;
let string = table
.get("description")
.and_then(Value::as_str)
.ok_or_else(|| {
de::Error::custom("description field not found for unknown permission")
})?;
let criticality = Criticality::from_str(criticality_str).map_err(|_| {
de::Error::custom(format!(
"invalid `criticality` value found: {}",
criticality_str
))
})?;
Ok((criticality, string.to_string()))
}
_ => Err(de::Error::custom(format!(
"Unexpected value: {:?}",
deserialize_result
))),
}
}
}
impl Config {
pub fn from_file<P: AsRef<Path>>(config_path: P) -> Result<Self, Error> {
let cfg_result: Result<Self, Error> = fs::read_to_string(config_path.as_ref())
.context("could not open configuration file")
.map_err(Error::from)
.and_then(|file_content| {
Ok(toml::from_str(&file_content).context(format_err!(
"could not decode config file: {}, using default",
config_path.as_ref().to_string_lossy()
))?)
})
.and_then(|mut new_config: Self| {
new_config
.loaded_files
.push(config_path.as_ref().to_path_buf());
Ok(new_config)
});
cfg_result
}
pub fn decorate_with_cli(&mut self, cli: &ArgMatches<'static>) -> Result<(), Error> {
self.set_options(cli);
self.verbose = cli.is_present("verbose");
self.quiet = cli.is_present("quiet");
self.overall_force = cli.is_present("force");
self.force = self.overall_force;
self.bench = cli.is_present("bench");
self.open = cli.is_present("open");
self.json = cli.is_present("json");
self.html = cli.is_present("html");
if cli.is_present("test-all") {
self.read_apks()
.context("error loading all the downloaded APKs")?;
} else {
self.add_app_package(
cli.value_of("package")
.expect("expected a value for the package CLI attribute"),
);
}
Ok(())
}
fn set_options(&mut self, cli: &ArgMatches<'static>) {
if let Some(min_criticality) = cli.value_of("min_criticality") {
if let Ok(m) = min_criticality.parse() {
self.min_criticality = m;
} else {
print_warning(format!(
"The min_criticality option must be one of {}, {}, {}, {} or {}.\nUsing \
default.",
"warning".italic(),
"low".italic(),
"medium".italic(),
"high".italic(),
"critical".italic()
));
}
}
if let Some(threads) = cli.value_of("threads") {
match threads.parse() {
Ok(t) if t > 0_usize => {
self.threads = t;
}
_ => {
print_warning(format!(
"The threads option must be an integer between 1 and {}",
usize::max_value()
));
}
}
}
if let Some(downloads_folder) = cli.value_of("downloads") {
self.downloads_folder = PathBuf::from(downloads_folder);
}
if let Some(dist_folder) = cli.value_of("dist") {
self.dist_folder = PathBuf::from(dist_folder);
}
if let Some(results_folder) = cli.value_of("results") {
self.results_folder = PathBuf::from(results_folder);
}
if let Some(dex2jar_folder) = cli.value_of("dex2jar") {
self.dex2jar_folder = PathBuf::from(dex2jar_folder);
}
if let Some(jd_cmd_file) = cli.value_of("jd-cmd") {
self.jd_cmd_file = PathBuf::from(jd_cmd_file);
}
if let Some(template_name) = cli.value_of("template") {
self.template = template_name.to_owned();
}
if let Some(rules_json) = cli.value_of("rules") {
self.rules_json = PathBuf::from(rules_json);
}
}
fn read_apks(&mut self) -> Result<(), Error> {
let iter = fs::read_dir(&self.downloads_folder)?;
for entry in iter {
match entry {
Ok(entry) => {
if let Some(ext) = entry.path().extension() {
if ext == "apk" {
self.add_app_package(
entry
.path()
.file_stem()
.expect("expected file stem for apk file")
.to_string_lossy()
.into_owned(),
)
}
}
}
Err(e) => {
print_warning(format!(
"there was an error when reading the downloads folder: {}",
e
));
}
}
}
Ok(())
}
pub fn check(&self) -> bool {
let check = self.downloads_folder.exists()
&& self.dex2jar_folder.exists()
&& self.jd_cmd_file.exists()
&& self.template_path().exists()
&& self.rules_json.exists();
if check {
for package in &self.app_packages {
if !package.exists() {
return false;
}
}
true
} else {
false
}
}
pub fn errors(&self) -> Vec<String> {
let mut errors = Vec::new();
if !self.downloads_folder.exists() {
errors.push(format!(
"The downloads folder `{}` does not exist",
self.downloads_folder.display()
));
}
for package in &self.app_packages {
if !package.exists() {
errors.push(format!(
"The APK file `{}` does not exist",
package.display()
));
}
}
if !self.dex2jar_folder.exists() {
errors.push(format!(
"The Dex2Jar folder `{}` does not exist",
self.dex2jar_folder.display()
));
}
if !self.jd_cmd_file.exists() {
errors.push(format!(
"The jd-cmd file `{}` does not exist",
self.jd_cmd_file.display()
));
}
if !self.templates_folder.exists() {
errors.push(format!(
"the templates folder `{}` does not exist",
self.templates_folder.display()
));
}
if !self.template_path().exists() {
errors.push(format!(
"the template `{}` does not exist in `{}`",
self.template,
self.templates_folder.display()
));
}
if !self.rules_json.exists() {
errors.push(format!(
"The `{}` rule file does not exist",
self.rules_json.display()
));
}
errors
}
pub fn loaded_config_files(&self) -> VecIter<'_, PathBuf> {
self.loaded_files.iter()
}
pub fn app_packages(&self) -> Vec<PathBuf> {
self.app_packages.clone()
}
pub(crate) fn add_app_package<P: AsRef<Path>>(&mut self, app_package: P) {
let mut package_path = self.downloads_folder.join(app_package);
if package_path.extension().is_none() {
let updated = package_path.set_extension("apk");
debug_assert!(
updated,
"did not update package path extension, no file name"
);
} else if package_path
.extension()
.expect("expected extension in package path")
!= "apk"
{
let mut file_name = package_path
.file_name()
.expect("expected file name in package path")
.to_string_lossy()
.into_owned();
file_name.push_str(".apk");
package_path.set_file_name(file_name);
}
self.app_packages.push(package_path);
}
pub fn is_verbose(&self) -> bool {
self.verbose
}
pub fn is_quiet(&self) -> bool {
self.quiet
}
pub fn is_force(&self) -> bool {
self.force
}
pub fn set_force(&mut self) {
self.force = true;
}
pub fn reset_force(&mut self) {
self.force = self.overall_force
}
pub fn is_bench(&self) -> bool {
self.bench
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn has_to_generate_json(&self) -> bool {
self.json
}
pub fn has_to_generate_html(&self) -> bool {
!self.json || self.html
}
pub fn min_criticality(&self) -> Criticality {
self.min_criticality
}
pub fn threads(&self) -> usize {
self.threads
}
pub fn dist_folder(&self) -> &Path {
&self.dist_folder
}
pub fn results_folder(&self) -> &Path {
&self.results_folder
}
pub fn dex2jar_folder(&self) -> &Path {
&self.dex2jar_folder
}
pub fn jd_cmd_file(&self) -> &Path {
&self.jd_cmd_file
}
pub fn template_path(&self) -> PathBuf {
self.templates_folder.join(&self.template)
}
pub fn templates_folder(&self) -> &Path {
&self.templates_folder
}
pub fn template_name(&self) -> &str {
&self.template
}
pub fn rules_json(&self) -> &Path {
&self.rules_json
}
pub fn unknown_permission_criticality(&self) -> Criticality {
self.unknown_permission.0
}
pub fn unknown_permission_description(&self) -> &str {
self.unknown_permission.1.as_str()
}
pub fn permissions(&self) -> Iter<'_, Permission> {
self.permissions.iter()
}
fn local_default() -> Self {
Self {
app_packages: Vec::new(),
verbose: false,
quiet: false,
overall_force: false,
force: false,
bench: false,
open: false,
json: false,
html: false,
threads: num_cpus::get(),
min_criticality: Criticality::Warning,
downloads_folder: PathBuf::from("."),
dist_folder: PathBuf::from("dist"),
results_folder: PathBuf::from("results"),
dex2jar_folder: Path::new("vendor").join("dex2jar-2.1-SNAPSHOT"),
jd_cmd_file: Path::new("vendor").join("jd-cmd.jar"),
templates_folder: PathBuf::from("templates"),
template: String::from("super"),
rules_json: PathBuf::from("rules.json"),
unknown_permission: (
Criticality::Low,
String::from(
"Even if the application can create its own \
permissions, it's discouraged, since it can \
lead to misunderstanding between developers.",
),
),
permissions: BTreeSet::new(),
loaded_files: Vec::new(),
}
}
}
impl Default for Config {
#[cfg(target_family = "unix")]
fn default() -> Self {
let mut config = Self::local_default();
let etc_rules = PathBuf::from("/etc/super-analyzer/rules.json");
if etc_rules.exists() {
config.rules_json = etc_rules;
}
let share_path = Path::new(if cfg!(target_os = "macos") {
"/usr/local/super-analyzer"
} else {
"/usr/share/super-analyzer"
});
if share_path.exists() {
config.dex2jar_folder = share_path.join("vendor/dex2jar-2.1-SNAPSHOT");
config.jd_cmd_file = share_path.join("vendor/jd-cmd.jar");
config.templates_folder = share_path.join("templates");
}
config
}
#[cfg(target_family = "windows")]
fn default() -> Self {
Config::local_default()
}
}
#[derive(Debug, Ord, Eq, Deserialize)]
pub struct Permission {
name: manifest::Permission,
criticality: Criticality,
label: String,
description: String,
}
impl PartialEq for Permission {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl PartialOrd for Permission {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.name.cmp(&other.name))
}
}
impl Permission {
pub fn name(&self) -> manifest::Permission {
self.name
}
pub fn criticality(&self) -> Criticality {
self.criticality
}
pub fn label(&self) -> &str {
self.label.as_str()
}
pub fn description(&self) -> &str {
self.description.as_str()
}
}
#[cfg(test)]
mod tests {
use std::{
fs,
path::{Path, PathBuf},
};
use num_cpus;
use super::Config;
use crate::{criticality::Criticality, static_analysis::manifest};
#[allow(clippy::cognitive_complexity)]
#[test]
fn it_config() {
let mut config = Config::default();
assert!(config.app_packages().is_empty());
assert!(!config.is_verbose());
assert!(!config.is_quiet());
assert!(!config.is_force());
assert!(!config.is_bench());
assert!(!config.is_open());
assert_eq!(config.threads(), num_cpus::get());
assert_eq!(config.downloads_folder, Path::new("."));
assert_eq!(config.dist_folder(), Path::new("dist"));
assert_eq!(config.results_folder(), Path::new("results"));
assert_eq!(config.template_name(), "super");
let share_path = Path::new(if cfg!(target_os = "macos") {
"/usr/local/super-analyzer"
} else if cfg!(target_family = "windows") {
""
} else {
"/usr/share/super-analyzer"
});
let share_path = if share_path.exists() {
share_path
} else {
Path::new("")
};
assert_eq!(
config.dex2jar_folder(),
share_path.join("vendor").join("dex2jar-2.1-SNAPSHOT")
);
assert_eq!(
config.jd_cmd_file(),
share_path.join("vendor").join("jd-cmd.jar")
);
assert_eq!(config.templates_folder(), share_path.join("templates"));
assert_eq!(
config.template_path(),
share_path.join("templates").join("super")
);
if cfg!(target_family = "unix") && Path::new("/etc/super-analyzer/rules.json").exists() {
assert_eq!(
config.rules_json(),
Path::new("/etc/super-analyzer/rules.json")
);
} else {
assert_eq!(config.rules_json(), Path::new("rules.json"));
}
assert_eq!(config.unknown_permission_criticality(), Criticality::Low);
assert_eq!(
config.unknown_permission_description(),
"Even if the application can create its own permissions, it's discouraged, \
since it can lead to misunderstanding between developers."
);
assert_eq!(config.permissions().next(), None);
if !config.downloads_folder.exists() {
fs::create_dir(&config.downloads_folder).unwrap();
}
if !config.dist_folder().exists() {
fs::create_dir(config.dist_folder()).unwrap();
}
if !config.results_folder().exists() {
fs::create_dir(config.results_folder()).unwrap();
}
config.add_app_package("test_app");
config.verbose = true;
config.quiet = true;
config.force = true;
config.bench = true;
config.open = true;
let packages = config.app_packages();
assert_eq!(&packages[0], &config.downloads_folder.join("test_app.apk"));
assert!(config.is_verbose());
assert!(config.is_quiet());
assert!(config.is_force());
assert!(config.is_bench());
assert!(config.is_open());
config.reset_force();
assert!(!config.is_force());
config.overall_force = true;
config.reset_force();
assert!(config.is_force());
if packages[0].exists() {
fs::remove_file(&packages[0]).unwrap();
}
assert!(!config.check());
let _ = fs::File::create(&packages[0]).unwrap();
assert!(config.check());
let config = Config::default();
assert!(config.check());
fs::remove_file(&packages[0]).unwrap();
}
#[test]
fn it_config_sample() {
let mut config = Config::from_file(&PathBuf::from("config.toml.sample")).unwrap();
config.add_app_package("test_app");
assert_eq!(config.threads(), 2);
assert_eq!(config.downloads_folder, Path::new("downloads"));
assert_eq!(config.dist_folder(), Path::new("dist"));
assert_eq!(config.results_folder(), Path::new("results"));
assert_eq!(
config.dex2jar_folder(),
Path::new("/usr/share/super-analyzer/vendor/dex2jar-2.1-SNAPSHOT")
);
assert_eq!(
config.jd_cmd_file(),
Path::new("/usr/share/super-analyzer/vendor/jd-cmd.jar")
);
assert_eq!(
config.templates_folder(),
Path::new("/usr/share/super-analyzer/templates")
);
assert_eq!(
config.template_path(),
Path::new("/usr/share/super-analyzer/templates/super")
);
assert_eq!(config.template_name(), "super");
assert_eq!(
config.rules_json(),
Path::new("/etc/super-analyzer/rules.json")
);
assert_eq!(config.unknown_permission_criticality(), Criticality::Low);
assert_eq!(
config.unknown_permission_description(),
"Even if the application can create its own permissions, it's discouraged, \
since it can lead to misunderstanding between developers."
);
let permission = config.permissions().next().unwrap();
assert_eq!(
permission.name(),
manifest::Permission::AndroidPermissionInternet
);
assert_eq!(permission.criticality(), Criticality::Warning);
assert_eq!(permission.label(), "Internet permission");
assert_eq!(
permission.description(),
"Allows the app to create network sockets and use custom network protocols. \
The browser and other applications provide means to send data to the \
internet, so this permission is not required to send data to the internet. \
Check if the permission is actually needed."
);
}
#[test]
fn it_generates_html_but_not_json_by_default() {
let mut final_config = Config::default();
final_config.html = false;
final_config.json = false;
assert!(final_config.has_to_generate_html());
assert!(!final_config.has_to_generate_json());
}
}