diff --git a/build.gradle b/build.gradle index affc8cf..fe6a9b5 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,7 @@ dependencies { compile "org.fusesource.jansi:jansi:${jansiVersion}" compile "org.apache.commons:commons-lang3:${commonsLangVersion}" compile "org.apache.commons:commons-text:${commonsTextVersion}" + compile "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" } jar { diff --git a/dependency-versions.gradle b/dependency-versions.gradle index 2156b4b..26bf82a 100644 --- a/dependency-versions.gradle +++ b/dependency-versions.gradle @@ -8,4 +8,5 @@ ext { jansiVersion = '1.17.1' commonsLangVersion = '3.8.1' commonsTextVersion = '1.6' + jacksonVersion = '2.9.8' } \ No newline at end of file diff --git a/src/main/java/com/bartek/esa/EsaMain.java b/src/main/java/com/bartek/esa/EsaMain.java index ef18130..ad51531 100644 --- a/src/main/java/com/bartek/esa/EsaMain.java +++ b/src/main/java/com/bartek/esa/EsaMain.java @@ -2,16 +2,25 @@ package com.bartek.esa; import com.bartek.esa.analyser.apk.ApkAnalyser; import com.bartek.esa.analyser.source.SourceAnalyser; -import com.bartek.esa.cli.model.CliArgsOptions; +import com.bartek.esa.cli.model.object.CliArgsOptions; import com.bartek.esa.cli.parser.CliArgsParser; import com.bartek.esa.core.model.object.Issue; import com.bartek.esa.di.DaggerDependencyInjector; import com.bartek.esa.dispatcher.dispatcher.MethodDispatcher; import com.bartek.esa.dispatcher.model.DispatcherActions; +import com.bartek.esa.error.EsaException; +import com.bartek.esa.formatter.archetype.Formatter; import com.bartek.esa.formatter.provider.FormatterProvider; +import io.vavr.control.Try; import javax.inject.Inject; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; public class EsaMain { @@ -39,9 +48,15 @@ public class EsaMain { CliArgsOptions options = cliArgsParser.parse(args); Set issues = methodDispatcher.dispatchMethod(options, dispatcherActions); Set filteredIssues = filterIssuesBySeverity(options, issues); - formatterProvider.provide(options).format(filteredIssues); + Formatter formatter = formatterProvider.provide(options); + formatter.beforeFormat(); + String output = formatter.format(filteredIssues); + displayOutputOrSaveToFile(options, output); + formatter.afterFormat(); - exitWithErrorIfAnyIssueIsAnError(filteredIssues); + if(options.isStrictMode()) { + exitWithErrorIfAnyIssueIsAnError(filteredIssues); + } } private Set filterIssuesBySeverity(CliArgsOptions options, Set issues) { @@ -50,6 +65,24 @@ public class EsaMain { .collect(Collectors.toSet()); } + private void displayOutputOrSaveToFile(CliArgsOptions options, String output) { + Optional.ofNullable(options.getOut()) + .map(this::getWriter) + .ifPresentOrElse(writeString(output), () -> System.out.println(output)); + } + + private BufferedWriter getWriter(File file) { + return Try.of(() -> new BufferedWriter(new FileWriter(file))) + .getOrElseThrow(EsaException::new); + } + + private Consumer writeString(String string) { + return writer -> Try.run(() -> { + writer.write(string); + writer.close(); + }).getOrElseThrow(EsaException::new); + } + private void exitWithErrorIfAnyIssueIsAnError(Set issues) { if(issues.stream().anyMatch(i -> i.getSeverity().isExitWithError())) { System.exit(1); diff --git a/src/main/java/com/bartek/esa/cli/model/enumeration/OutputType.java b/src/main/java/com/bartek/esa/cli/model/enumeration/OutputType.java new file mode 100644 index 0000000..a27e1c9 --- /dev/null +++ b/src/main/java/com/bartek/esa/cli/model/enumeration/OutputType.java @@ -0,0 +1,7 @@ +package com.bartek.esa.cli.model.enumeration; + +public enum OutputType { + DEFAULT, + COLOR, + JSON; +} diff --git a/src/main/java/com/bartek/esa/cli/model/CliArgsOptions.java b/src/main/java/com/bartek/esa/cli/model/object/CliArgsOptions.java similarity index 71% rename from src/main/java/com/bartek/esa/cli/model/CliArgsOptions.java rename to src/main/java/com/bartek/esa/cli/model/object/CliArgsOptions.java index a9ff835..9990e97 100644 --- a/src/main/java/com/bartek/esa/cli/model/CliArgsOptions.java +++ b/src/main/java/com/bartek/esa/cli/model/object/CliArgsOptions.java @@ -1,8 +1,10 @@ -package com.bartek.esa.cli.model; +package com.bartek.esa.cli.model.object; +import com.bartek.esa.cli.model.enumeration.OutputType; import lombok.Builder; import lombok.Data; +import java.io.File; import java.util.Set; import static java.util.Collections.emptySet; @@ -14,9 +16,11 @@ public class CliArgsOptions { private String apkAuditFile; private Set excludes; private Set plugins; - private boolean color; + private OutputType outputType; private Set severities; private boolean debug; + private File out; + private boolean strictMode; public boolean isSourceAnalysis() { return sourceAnalysisDirectory != null; @@ -30,6 +34,8 @@ public class CliArgsOptions { return CliArgsOptions.builder() .excludes(emptySet()) .plugins(emptySet()) + .severities(emptySet()) + .outputType(OutputType.DEFAULT) .build(); } } diff --git a/src/main/java/com/bartek/esa/cli/parser/CliArgsParser.java b/src/main/java/com/bartek/esa/cli/parser/CliArgsParser.java index e4a955c..ae7c240 100644 --- a/src/main/java/com/bartek/esa/cli/parser/CliArgsParser.java +++ b/src/main/java/com/bartek/esa/cli/parser/CliArgsParser.java @@ -1,11 +1,14 @@ package com.bartek.esa.cli.parser; -import com.bartek.esa.cli.model.CliArgsOptions; +import com.bartek.esa.cli.model.enumeration.OutputType; +import com.bartek.esa.cli.model.object.CliArgsOptions; import com.bartek.esa.cli.printer.PluginPrinter; import com.bartek.esa.core.model.enumeration.Severity; +import io.vavr.control.Try; import org.apache.commons.cli.*; import javax.inject.Inject; +import java.io.File; import java.util.HashSet; import java.util.stream.Collectors; @@ -19,10 +22,12 @@ public class CliArgsParser { private static final String EXCLUDE_OPT = "exclude"; private static final String HELP_OPT = "help"; private static final String PLUGINS_OPT = "plugins"; - private static final String COLOR_OPT = "color"; + private static final String FORMAT_OPT = "format"; + private static final String OUT_OPT = "out"; private static final String SEVERITIES_OPT = "severities"; private static final String DEBUG_OPT = "debug"; private static final String LIST_PLUGINS_OPT = "list-plugins"; + private static final String STRICT_OPT = "strict"; private final PluginPrinter pluginPrinter; @@ -66,12 +71,23 @@ public class CliArgsParser { .apkAuditFile(command.hasOption(APK_OPT) ? command.getOptionValue(APK_OPT) : null) .plugins(command.hasOption(PLUGINS_OPT) ? new HashSet<>(asList(command.getOptionValues(PLUGINS_OPT))) : emptySet()) .excludes(command.hasOption(EXCLUDE_OPT) ? new HashSet<>(asList(command.getOptionValues(EXCLUDE_OPT))) : emptySet()) - .color(command.hasOption(COLOR_OPT)) + .outputType(getOutputTypeForOptions(command)) .severities(command.hasOption(SEVERITIES_OPT) ? new HashSet<>(asList((command.getOptionValues(SEVERITIES_OPT)))) : stream(Severity.values()).map(Severity::name).collect(Collectors.toSet())) .debug(command.hasOption(DEBUG_OPT)) + .out(command.hasOption(OUT_OPT) ? new File(command.getOptionValue(OUT_OPT)) : null) + .strictMode(command.hasOption(STRICT_OPT)) .build(); } + private OutputType getOutputTypeForOptions(CommandLine command) { + if(command.hasOption(FORMAT_OPT)) { + return Try.of(() -> OutputType.valueOf(command.getOptionValue(FORMAT_OPT).toUpperCase())) + .getOrElse(OutputType.DEFAULT); + } + + return OutputType.DEFAULT; + } + private void printHelp() { HelpFormatter formatter = new HelpFormatter(); formatter.printHelp("esa", prepareOptions(), true); @@ -84,10 +100,12 @@ public class CliArgsParser { options.addOption(exclude()); options.addOption(plugins()); options.addOption(help()); - options.addOption(color()); + options.addOption(format()); options.addOption(severities()); options.addOption(debug()); options.addOption(listPlugins()); + options.addOption(out()); + options.addOption(strict()); return options; } @@ -134,10 +152,12 @@ public class CliArgsParser { .build(); } - private Option color() { + private Option format() { return Option.builder() - .longOpt(COLOR_OPT) - .desc("enable colored output") + .longOpt(FORMAT_OPT) + .argName("OUTPUT_TYPE") + .numberOfArgs(1) + .desc("select format format (available: default, color, json)") .build(); } @@ -147,7 +167,7 @@ public class CliArgsParser { .longOpt(SEVERITIES_OPT) .argName("SEVERITY") .numberOfArgs(Option.UNLIMITED_VALUES) - .desc("filter output to selected severities(available: " + severities + ")") + .desc("filter format to selected severities(available: " + severities + ")") .build(); } @@ -164,4 +184,20 @@ public class CliArgsParser { .desc("list available plugins") .build(); } + + private Option out() { + return Option.builder() + .longOpt(OUT_OPT) + .argName("PATH") + .numberOfArgs(1) + .desc("optional output of analysis - recommended to use with non-text output format") + .build(); + } + + private Option strict() { + return Option.builder() + .longOpt(STRICT_OPT) + .desc("enable strict mode - return code depends on analysis result (recommended to use in batch mode)") + .build(); + } } diff --git a/src/main/java/com/bartek/esa/core/plugin/SqlInjectionPlugin.java b/src/main/java/com/bartek/esa/core/plugin/SqlInjectionPlugin.java index 2e9ab6b..670fc30 100644 --- a/src/main/java/com/bartek/esa/core/plugin/SqlInjectionPlugin.java +++ b/src/main/java/com/bartek/esa/core/plugin/SqlInjectionPlugin.java @@ -5,6 +5,7 @@ import com.bartek.esa.core.model.enumeration.Severity; import com.bartek.esa.core.xml.XmlHelper; import com.bartek.esa.file.matcher.GlobMatcher; import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.expr.Expression; import com.github.javaparser.ast.expr.MethodCallExpr; import javax.inject.Inject; @@ -20,6 +21,21 @@ public class SqlInjectionPlugin extends JavaPlugin { public void run(CompilationUnit compilationUnit) { compilationUnit.findAll(MethodCallExpr.class).stream() .filter(expr -> expr.getName().getIdentifier().equals("rawQuery")) + .filter(expr -> expr.getArguments().size() >= 2) + .filter(this::isConcatenationOrMethodCall) + .filter(this::ifMethodIsStringFormat) .forEach(expr -> addIssue(Severity.VULNERABILITY, getLineNumberFromExpression(expr), expr.toString())); } + + private boolean isConcatenationOrMethodCall(MethodCallExpr expr) { + return expr.getArguments().get(0).isBinaryExpr() || expr.getArguments().get(0).isMethodCallExpr(); + } + + private boolean ifMethodIsStringFormat(MethodCallExpr expr) { + if(expr.getArguments().get(0).isMethodCallExpr()) { + return expr.getArguments().get(0).asMethodCallExpr().getName().getIdentifier().equals("format"); + } + + return true; + } } diff --git a/src/main/java/com/bartek/esa/dispatcher/dispatcher/MethodDispatcher.java b/src/main/java/com/bartek/esa/dispatcher/dispatcher/MethodDispatcher.java index 8c294cd..362eae8 100644 --- a/src/main/java/com/bartek/esa/dispatcher/dispatcher/MethodDispatcher.java +++ b/src/main/java/com/bartek/esa/dispatcher/dispatcher/MethodDispatcher.java @@ -1,6 +1,6 @@ package com.bartek.esa.dispatcher.dispatcher; -import com.bartek.esa.cli.model.CliArgsOptions; +import com.bartek.esa.cli.model.object.CliArgsOptions; import com.bartek.esa.core.model.object.Issue; import com.bartek.esa.dispatcher.model.DispatcherActions; diff --git a/src/main/java/com/bartek/esa/formatter/archetype/Formatter.java b/src/main/java/com/bartek/esa/formatter/archetype/Formatter.java index a84e644..2a2ee18 100644 --- a/src/main/java/com/bartek/esa/formatter/archetype/Formatter.java +++ b/src/main/java/com/bartek/esa/formatter/archetype/Formatter.java @@ -5,5 +5,7 @@ import com.bartek.esa.core.model.object.Issue; import java.util.Set; public interface Formatter { - void format(Set issues); + void beforeFormat(); + String format(Set issues); + void afterFormat(); } diff --git a/src/main/java/com/bartek/esa/formatter/di/FormatterModule.java b/src/main/java/com/bartek/esa/formatter/di/FormatterModule.java index b7741aa..e51a869 100644 --- a/src/main/java/com/bartek/esa/formatter/di/FormatterModule.java +++ b/src/main/java/com/bartek/esa/formatter/di/FormatterModule.java @@ -2,6 +2,7 @@ package com.bartek.esa.formatter.di; import com.bartek.esa.core.desc.provider.DescriptionProvider; import com.bartek.esa.formatter.formatter.ColorFormatter; +import com.bartek.esa.formatter.formatter.JsonFormatter; import com.bartek.esa.formatter.formatter.SimpleFormatter; import com.bartek.esa.formatter.provider.FormatterProvider; import dagger.Module; @@ -21,7 +22,12 @@ public class FormatterModule { } @Provides - public FormatterProvider formatterProvider(SimpleFormatter simpleFormatter, ColorFormatter colorFormatter) { - return new FormatterProvider(simpleFormatter, colorFormatter); + public JsonFormatter jsonFormatter() { + return new JsonFormatter(); + } + + @Provides + public FormatterProvider formatterProvider(SimpleFormatter simpleFormatter, ColorFormatter colorFormatter, JsonFormatter jsonFormatter) { + return new FormatterProvider(simpleFormatter, colorFormatter, jsonFormatter); } } diff --git a/src/main/java/com/bartek/esa/formatter/formatter/ColorFormatter.java b/src/main/java/com/bartek/esa/formatter/formatter/ColorFormatter.java index 8b56f46..33091aa 100644 --- a/src/main/java/com/bartek/esa/formatter/formatter/ColorFormatter.java +++ b/src/main/java/com/bartek/esa/formatter/formatter/ColorFormatter.java @@ -30,12 +30,15 @@ public class ColorFormatter implements Formatter { } @Override - public void format(Set issues) { + public void beforeFormat() { AnsiConsole.systemInstall(); + } + + @Override + public String format(Set issues) { if (issues.isEmpty()) { Ansi noIssuesFound = ansi().fg(GREEN).a("No issues found.").reset(); - System.out.println(noIssuesFound); - return; + return noIssuesFound.toString(); } String format = issues.stream() @@ -43,8 +46,11 @@ public class ColorFormatter implements Formatter { .map(this::format) .collect(Collectors.joining()); - System.out.println(format.substring(0, format.length() - 2)); - System.out.println(printSummary(issues)); + return String.format("%s\n%s", format.substring(0, format.length() - 2), printSummary(issues)); + } + + @Override + public void afterFormat() { AnsiConsole.systemUninstall(); } diff --git a/src/main/java/com/bartek/esa/formatter/formatter/JsonFormatter.java b/src/main/java/com/bartek/esa/formatter/formatter/JsonFormatter.java new file mode 100644 index 0000000..d94f88f --- /dev/null +++ b/src/main/java/com/bartek/esa/formatter/formatter/JsonFormatter.java @@ -0,0 +1,29 @@ +package com.bartek.esa.formatter.formatter; + +import com.bartek.esa.core.model.object.Issue; +import com.bartek.esa.error.EsaException; +import com.bartek.esa.formatter.archetype.Formatter; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vavr.control.Try; + +import java.util.Set; + +public class JsonFormatter implements Formatter { + + @Override + public void beforeFormat() { + // nothing to do + } + + @Override + public String format(Set issues) { + ObjectMapper objectMapper = new ObjectMapper(); + return Try.of(() -> objectMapper.writeValueAsString(issues)) + .getOrElseThrow(EsaException::new); + } + + @Override + public void afterFormat() { + // nothing to do + } +} diff --git a/src/main/java/com/bartek/esa/formatter/formatter/SimpleFormatter.java b/src/main/java/com/bartek/esa/formatter/formatter/SimpleFormatter.java index 7644ce0..448284b 100644 --- a/src/main/java/com/bartek/esa/formatter/formatter/SimpleFormatter.java +++ b/src/main/java/com/bartek/esa/formatter/formatter/SimpleFormatter.java @@ -20,10 +20,14 @@ public class SimpleFormatter implements Formatter { } @Override - public void format(Set issues) { + public void beforeFormat() { + // nothing to do + } + + @Override + public String format(Set issues) { if (issues.isEmpty()) { - System.out.println("No issues found."); - return; + return "No issues found."; } String format = issues.stream() @@ -31,8 +35,12 @@ public class SimpleFormatter implements Formatter { .map(this::format) .collect(Collectors.joining()); - System.out.println(format.substring(0, format.length() - 2)); - System.out.println(printSummary(issues)); + return String.format("%s\n%s", format.substring(0, format.length() - 2), printSummary(issues)); + } + + @Override + public void afterFormat() { + // nothing to do } private String format(Issue issue) { diff --git a/src/main/java/com/bartek/esa/formatter/provider/FormatterProvider.java b/src/main/java/com/bartek/esa/formatter/provider/FormatterProvider.java index 16a31ce..0b1ad1c 100644 --- a/src/main/java/com/bartek/esa/formatter/provider/FormatterProvider.java +++ b/src/main/java/com/bartek/esa/formatter/provider/FormatterProvider.java @@ -1,8 +1,9 @@ package com.bartek.esa.formatter.provider; -import com.bartek.esa.cli.model.CliArgsOptions; +import com.bartek.esa.cli.model.object.CliArgsOptions; import com.bartek.esa.formatter.archetype.Formatter; import com.bartek.esa.formatter.formatter.ColorFormatter; +import com.bartek.esa.formatter.formatter.JsonFormatter; import com.bartek.esa.formatter.formatter.SimpleFormatter; import javax.inject.Inject; @@ -10,14 +11,24 @@ import javax.inject.Inject; public class FormatterProvider { private final SimpleFormatter simpleFormatter; private final ColorFormatter colorFormatter; + private final JsonFormatter jsonFormatter; @Inject - public FormatterProvider(SimpleFormatter simpleFormatter, ColorFormatter colorFormatter) { + public FormatterProvider(SimpleFormatter simpleFormatter, ColorFormatter colorFormatter, JsonFormatter jsonFormatter) { this.simpleFormatter = simpleFormatter; this.colorFormatter = colorFormatter; + this.jsonFormatter = jsonFormatter; } public Formatter provide(CliArgsOptions options) { - return options.isColor() ? colorFormatter : simpleFormatter; + switch (options.getOutputType()) { + case COLOR: + return colorFormatter; + case JSON: + return jsonFormatter; + case DEFAULT: + default: + return simpleFormatter; + } } }