17: Reinvent formatting system (+ add JSON formatter)

This commit is contained in:
Bartłomiej Pluta
2019-06-07 12:29:40 +02:00
parent 32d0c1f16f
commit e6265927f0
14 changed files with 192 additions and 30 deletions

View File

@@ -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 {

View File

@@ -8,4 +8,5 @@ ext {
jansiVersion = '1.17.1'
commonsLangVersion = '3.8.1'
commonsTextVersion = '1.6'
jacksonVersion = '2.9.8'
}

View File

@@ -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,10 +48,16 @@ public class EsaMain {
CliArgsOptions options = cliArgsParser.parse(args);
Set<Issue> issues = methodDispatcher.dispatchMethod(options, dispatcherActions);
Set<Issue> 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();
if(options.isStrictMode()) {
exitWithErrorIfAnyIssueIsAnError(filteredIssues);
}
}
private Set<Issue> filterIssuesBySeverity(CliArgsOptions options, Set<Issue> issues) {
return issues.stream()
@@ -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<BufferedWriter> writeString(String string) {
return writer -> Try.run(() -> {
writer.write(string);
writer.close();
}).getOrElseThrow(EsaException::new);
}
private void exitWithErrorIfAnyIssueIsAnError(Set<Issue> issues) {
if(issues.stream().anyMatch(i -> i.getSeverity().isExitWithError())) {
System.exit(1);

View File

@@ -0,0 +1,7 @@
package com.bartek.esa.cli.model.enumeration;
public enum OutputType {
DEFAULT,
COLOR,
JSON;
}

View File

@@ -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<String> excludes;
private Set<String> plugins;
private boolean color;
private OutputType outputType;
private Set<String> 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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -5,5 +5,7 @@ import com.bartek.esa.core.model.object.Issue;
import java.util.Set;
public interface Formatter {
void format(Set<Issue> issues);
void beforeFormat();
String format(Set<Issue> issues);
void afterFormat();
}

View File

@@ -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);
}
}

View File

@@ -30,12 +30,15 @@ public class ColorFormatter implements Formatter {
}
@Override
public void format(Set<Issue> issues) {
public void beforeFormat() {
AnsiConsole.systemInstall();
}
@Override
public String format(Set<Issue> 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();
}

View File

@@ -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<Issue> issues) {
ObjectMapper objectMapper = new ObjectMapper();
return Try.of(() -> objectMapper.writeValueAsString(issues))
.getOrElseThrow(EsaException::new);
}
@Override
public void afterFormat() {
// nothing to do
}
}

View File

@@ -20,10 +20,14 @@ public class SimpleFormatter implements Formatter {
}
@Override
public void format(Set<Issue> issues) {
public void beforeFormat() {
// nothing to do
}
@Override
public String format(Set<Issue> 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) {

View File

@@ -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;
}
}
}