From c289cc6b82f83a675cc873221a8c65fd385d2b53 Mon Sep 17 00:00:00 2001 From: Rahul Krishna Date: Tue, 14 May 2024 12:27:53 -0400 Subject: [PATCH] Enable analysis over a single file provided as a string to codeanalyzer. Signed-off-by: Rahul Krishna --- .../java/com/ibm/northstar/CodeAnalyzer.java | 95 +++++++++++-------- .../java/com/ibm/northstar/SymbolTable.java | 29 +++++- .../ibm/northstar/SystemDependencyGraph.java | 1 - .../ibm/northstar/entities/SystemDepEdge.java | 15 +++ 4 files changed, 97 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/ibm/northstar/CodeAnalyzer.java b/src/main/java/com/ibm/northstar/CodeAnalyzer.java index 787f051c..1049997e 100644 --- a/src/main/java/com/ibm/northstar/CodeAnalyzer.java +++ b/src/main/java/com/ibm/northstar/CodeAnalyzer.java @@ -43,27 +43,27 @@ @Command(name = "codeanalyzer", mixinStandardHelpOptions = true, sortOptions = false, version = "codeanalyzer v1.1", description = "Convert java binary (*.jar, *.ear, *.war) into a comprehensive system dependency graph.") public class CodeAnalyzer implements Runnable { - @Option(names = {"-i", "--input"}, required = true, description = "Path to the project root directory.") + @Option(names = {"-i", "--input"}, description = "Path to the project root directory.") private static String input; + @Option(names = {"-s", "--source-analysis"}, description = "Analyze a single string of java source code instead the project.") + private static String sourceAnalysis; + + @Option(names = {"-o", "--output"}, description = "Destination directory to save the output graphs. By default, the SDG formatted as a JSON will be printed to the console.") + private static String output; + @Option(names = {"-b", "--build-cmd"}, description = "Custom build command. Defaults to auto build.") private static String build; @Option(names = {"--no-build"}, description = "Do not build your application. Use this option if you have already built your application.") private static boolean noBuild = false; - @Option(names = {"-a", "--analysis-level"}, description = "[Optional] Level of analysis to perform. Options: 1 (for just symbol table) or 2 (for full analysis including the system depenedency graph). Default: 1") + @Option(names = {"-a", "--analysis-level"}, description = "Level of analysis to perform. Options: 1 (for just symbol table) or 2 (for full analysis including the system depenedency graph). Default: 1") private static int analysisLevel = 1; - @Option(names = {"-o", "--output"}, description = "[Optional] Destination directory to save the output graphs. By default, the SDG formatted as a JSON will be printed to the console.") - private static String output; - - @Option(names = {"-d", "--dependencies"}, description = "[Optional] Path to the application 3rd party dependencies that may be helpful in analyzing the application.") + @Option(names = {"-d", "--dependencies"}, description = "Path to the application 3rd party dependencies that may be helpful in analyzing the application.") private static String dependencies; - @Option(names = {"-s", "--source-analysis"}, description = "[Experimental] Analyze the source code instead directly of the binary. Warning: This option is experimental and may not work as expected.") - private static boolean analyzeSource = false; - @Option(names = {"-v", "--verbose"}, description = "Print logs to console.") private static boolean verbose = false; @@ -96,42 +96,55 @@ public void run() { private static void analyze() throws IOException, ClassHierarchyException, CallGraphBuilderCancelException { - // download library dependencies of project for type resolution - if (!BuildProject.downloadLibraryDependencies(input)) { - Log.warn("Failed to download library dependencies of project"); + JsonObject combinedJsonObject = new JsonObject(); + Map symbolTable; + // First of all if, sourceAnalysis is provided, we will analyze the source code instead of the project. + if (sourceAnalysis != null) { + // Construct symbol table for source code + Log.debug("Single file analysis."); + Pair, Map>> symbolTableExtractionResult = SymbolTable.extractSingle(sourceAnalysis); + symbolTable = symbolTableExtractionResult.getLeft(); } - // construct symbol table for project, write parse problems to file in output directory if specified - Pair, Map>> symbolTableExtractionResult = - SymbolTable.extractAll(Paths.get(input)); - Map symbolTable = symbolTableExtractionResult.getLeft(); - if (output != null) { - Path outputPath = Paths.get(output); - if (!Files.exists(outputPath)) { - Files.createDirectories(outputPath); + + else { + + // download library dependencies of project for type resolution + if (!BuildProject.downloadLibraryDependencies(input)) { + Log.warn("Failed to download library dependencies of project"); + } + // construct symbol table for project, write parse problems to file in output directory if specified + Pair, Map>> symbolTableExtractionResult = + SymbolTable.extractAll(Paths.get(input)); + + symbolTable = symbolTableExtractionResult.getLeft(); + if (output != null) { + Path outputPath = Paths.get(output); + if (!Files.exists(outputPath)) { + Files.createDirectories(outputPath); + } + gson.toJson(symbolTableExtractionResult.getRight(), new FileWriter(new File(outputPath.toString(), "parse_errors.json"))); } - gson.toJson(symbolTableExtractionResult.getRight(), new FileWriter(new File(outputPath.toString(), "parse_errors.json"))); - } - JsonObject combinedJsonObject = new JsonObject(); - if (analysisLevel > 1) { - // Save SDG, IPCFG, and Call graph as JSON - // If noBuild is not true, and build is also not provided, we will use "auto" as the build command - build = build == null ? "auto" : build; - // Is noBuild is true, we will not build the project - build = noBuild ? null : build; - String sdgAsJSONString = SystemDependencyGraph.construct(input, dependencies, build); - JsonElement sdgAsJSONElement = gson.fromJson(sdgAsJSONString, JsonElement.class); - JsonObject sdgAsJSONObject = sdgAsJSONElement.getAsJsonObject(); - - // We don't really need these fields, so we'll remove it. - sdgAsJSONObject.remove("nodes"); - sdgAsJSONObject.remove("creator"); - sdgAsJSONObject.remove("version"); - - // Remove the 'edges' element and move the list of edges up one level - JsonElement edges = sdgAsJSONObject.get("edges"); - combinedJsonObject.add("system_dependency_graph", edges); + if (analysisLevel > 1) { + // Save SDG, and Call graph as JSON + // If noBuild is not true, and build is also not provided, we will use "auto" as the build command + build = build == null ? "auto" : build; + // Is noBuild is true, we will not build the project + build = noBuild ? null : build; + String sdgAsJSONString = SystemDependencyGraph.construct(input, dependencies, build); + JsonElement sdgAsJSONElement = gson.fromJson(sdgAsJSONString, JsonElement.class); + JsonObject sdgAsJSONObject = sdgAsJSONElement.getAsJsonObject(); + + // We don't really need these fields, so we'll remove it. + sdgAsJSONObject.remove("nodes"); + sdgAsJSONObject.remove("creator"); + sdgAsJSONObject.remove("version"); + + // Remove the 'edges' element and move the list of edges up one level + JsonElement edges = sdgAsJSONObject.get("edges"); + combinedJsonObject.add("system_dependency_graph", edges); + } } // Convert the JavaCompilationUnit to JSON and add to consolidated json object diff --git a/src/main/java/com/ibm/northstar/SymbolTable.java b/src/main/java/com/ibm/northstar/SymbolTable.java index c14c3721..078d67f1 100644 --- a/src/main/java/com/ibm/northstar/SymbolTable.java +++ b/src/main/java/com/ibm/northstar/SymbolTable.java @@ -1,6 +1,8 @@ package com.ibm.northstar; +import com.github.javaparser.JavaParser; import com.github.javaparser.ParseResult; +import com.github.javaparser.ParserConfiguration; import com.github.javaparser.Problem; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.NodeList; @@ -16,6 +18,8 @@ import com.github.javaparser.resolution.UnsolvedSymbolException; import com.github.javaparser.resolution.types.ResolvedType; import com.github.javaparser.symbolsolver.JavaSymbolSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver; import com.github.javaparser.symbolsolver.utils.SymbolSolverCollectionStrategy; import com.github.javaparser.utils.ProjectRoot; import com.github.javaparser.utils.SourceRoot; @@ -90,7 +94,7 @@ private boolean isMethodSignatureMatch(String fullSignature, String searchSignat private static JavaCompilationUnit processCompilationUnit(CompilationUnit parseResult) { JavaCompilationUnit cUnit = new JavaCompilationUnit(); - cUnit.setFilePath(parseResult.getStorage().get().getFileName()); + cUnit.setFilePath(parseResult.getStorage().map(s -> s.getPath().toString()).orElse("")); // Add the comment field to the compilation unit cUnit.setComment(parseResult.getComment().isPresent() ? parseResult.getComment().get().asString() : ""); @@ -573,6 +577,29 @@ public static Pair, Map>> return Pair.of(symbolTable, parseProblems); } + public static Pair, Map>> extractSingle(String code) throws IOException { + Map symbolTable = new LinkedHashMap(); + Map parseProblems = new HashMap>(); + // Setting up symbol solvers + CombinedTypeSolver combinedTypeSolver = new CombinedTypeSolver(); + combinedTypeSolver.add(new ReflectionTypeSolver()); + + ParserConfiguration parserConfiguration = new ParserConfiguration(); + parserConfiguration.setSymbolResolver(new JavaSymbolSolver(combinedTypeSolver)); + + JavaParser javaParser = new JavaParser(parserConfiguration); + ParseResult parseResult = javaParser.parse(code); + if (parseResult.isSuccessful()) { + CompilationUnit compilationUnit = parseResult.getResult().get(); + Log.debug("Successfully parsed code. Now processing compilation unit"); + symbolTable.put("", processCompilationUnit(compilationUnit)); + } else { + Log.error(parseResult.getProblems().toString()); + parseProblems.put("code", parseResult.getProblems()); + } + return Pair.of(symbolTable, parseProblems); + } + public static void main(String[] args) throws IOException { extractAll(Paths.get(args[0])); } diff --git a/src/main/java/com/ibm/northstar/SystemDependencyGraph.java b/src/main/java/com/ibm/northstar/SystemDependencyGraph.java index aa182a17..276c63b7 100644 --- a/src/main/java/com/ibm/northstar/SystemDependencyGraph.java +++ b/src/main/java/com/ibm/northstar/SystemDependencyGraph.java @@ -140,7 +140,6 @@ private static org.jgrapht.Graph, AbstractGraphEdge> buil Pair target = Optional.ofNullable(getCallableFromSymbolTable(s.getNode().getMethod())).orElseGet(() -> createAndPutNewCallableInSymbolTable(s.getNode().getMethod())); graph.addVertex(target); - String edgeType = edgeLabels.apply(p, s); SystemDepEdge graphEdge = new SystemDepEdge(p, s, edgeType); SystemDepEdge cgEdge = (SystemDepEdge) graph.getEdge(source, target); diff --git a/src/main/java/com/ibm/northstar/entities/SystemDepEdge.java b/src/main/java/com/ibm/northstar/entities/SystemDepEdge.java index 6c30326d..a53192b7 100644 --- a/src/main/java/com/ibm/northstar/entities/SystemDepEdge.java +++ b/src/main/java/com/ibm/northstar/entities/SystemDepEdge.java @@ -41,6 +41,8 @@ public class SystemDepEdge extends AbstractGraphEdge { * The Type. */ public final String type; + public final String sourceKind; + public final String destinationKind; /** * Instantiates a new System dep edge. @@ -51,6 +53,8 @@ public class SystemDepEdge extends AbstractGraphEdge { */ public SystemDepEdge(Statement sourceStatement, Statement destinationStatement, String type) { super(); + this.sourceKind = sourceStatement.getKind().toString(); + this.destinationKind = destinationStatement.getKind().toString(); this.type = type; this.sourcePos = getStatementPosition(sourceStatement); this.destinationPos = getStatementPosition(destinationStatement); @@ -69,6 +73,15 @@ public boolean equals(Object o) { && this.type.equals(((SystemDepEdge) o).getType()); } + + public String getSourceKind() { + return sourceKind; + } + + public String getDestinationKind() { + return destinationKind; + } + /** * Gets type. * @@ -98,7 +111,9 @@ public Integer getDestinationPos() { public Map getAttributes() { Map map = new LinkedHashMap<>(); + map.put("source_kind", DefaultAttribute.createAttribute(getSourceKind())); map.put("type", DefaultAttribute.createAttribute(getType())); + map.put("destination_kind", DefaultAttribute.createAttribute(getDestinationKind())); map.put("weight", DefaultAttribute.createAttribute(String.valueOf(getWeight()))); return map; }