Skip to content

Commit 4c14ca3

Browse files
committed
New Library API analysis
- parses library jar|aar files directly - improved public interface extractor - json export of analysis results per lib
1 parent bc27e74 commit 4c14ca3

File tree

12 files changed

+1307
-47
lines changed

12 files changed

+1307
-47
lines changed

logging/logback.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
<logger name="de.infsec.tpl.LibCodeUsage" level="debug"/>
1414
<logger name="de.infsec.tpl.eval.LibraryApiAnalysis" level="info"/>
1515

16+
<logger name="de.infsec.tpl.modules.libapi" level="info"/>
17+
18+
1619
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
1720
<encoder>
1821
<pattern>%d{HH:mm:ss} %-5level %-25logger{0} : %msg%n</pattern>

src/de/infsec/tpl/TplCLI.java

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Collections;
2121
import java.util.List;
2222

23+
import de.infsec.tpl.modules.libapi.LibraryApiAnalysis;
2324
import org.apache.commons.cli.BasicParser;
2425
import org.apache.commons.cli.CommandLine;
2526
import org.apache.commons.cli.CommandLineParser;
@@ -36,7 +37,6 @@
3637
import ch.qos.logback.core.util.StatusPrinter;
3738
import de.infsec.tpl.stats.SQLStats;
3839
import de.infsec.tpl.utils.Utils;
39-
import de.infsec.tpl.eval.LibraryApiAnalysis;
4040
import de.infsec.tpl.profile.LibProfile;
4141

4242

@@ -49,7 +49,7 @@ public class TplCLI {
4949
* - PROFILE: generate library profiles from original lib SDKs and descriptions
5050
* - MATCH: match lib profiles in provided apps
5151
* - DB: build sqlite database from app stat files
52-
* - LIB_API_ANALYSIS: analyzes library api robustness (api additions, removals, changes)
52+
* - LIB_API_ANALYSIS: analyzes library api stability (api additions, removals, changes)
5353
*/
5454
public static enum OpMode {PROFILE, MATCH, DB, LIB_API_ANALYSIS};
5555

@@ -110,7 +110,7 @@ public static class CliOptions {
110110
private static final String USAGE_PROFILE = TOOLNAME + " --opmode profile -a <path-to-android.jar> -x <path-to-lib-desc> <options> $lib.[jar|aar]";
111111
private static final String USAGE_MATCH = TOOLNAME + " --opmode match -a <path-to-android.jar> <options> $path-to-app(-dir)";
112112
private static final String USAGE_DB = TOOLNAME + " --opmode db -p <path-to-profiles> -s <path-to-stats>";
113-
private static final String USAGE_LIB_API_ANALYSIS = TOOLNAME + " --opmode lib_api_analysis -p <path-to-profiles> -j <output-dir>";
113+
private static final String USAGE_LIB_API_ANALYSIS = TOOLNAME + " --opmode lib_api_analysis -a <path-to-android.jar> $path-to-lib-sdks";
114114

115115
private static ArrayList<File> inputFiles;
116116
private static File libraryDescription = null;
@@ -124,7 +124,6 @@ public static void main(String[] args) {
124124
// initialize logback
125125
initLogging();
126126

127-
128127
List<LibProfile> profiles = null;
129128

130129
// TODO MODE = LIB_UPDATABILITY
@@ -142,10 +141,6 @@ public static void main(String[] args) {
142141
break;
143142

144143
case LIB_API_ANALYSIS:
145-
profiles = loadLibraryProfiles();
146-
new LibraryApiAnalysis().run(profiles);
147-
System.exit(0);
148-
149144
case PROFILE:
150145
}
151146

@@ -156,7 +151,10 @@ public static void main(String[] args) {
156151
new LibraryIdentifier(inputFile).identifyLibraries(profiles);
157152

158153
} else if (CliOptions.opmode.equals(OpMode.PROFILE)) {
159-
new LibraryProfiler(inputFile, libraryDescription).extractFingerPrints();
154+
new LibraryProfiler(inputFile, libraryDescription).extractFingerPrints();
155+
156+
} else if (CliOptions.opmode.equals(OpMode.LIB_API_ANALYSIS)) {
157+
new LibraryApiAnalysis(inputFile);
160158
}
161159
} catch (Throwable t) {
162160
logger.error("[FATAL " + (t instanceof Exception? "EXCEPTION" : "ERROR") + "] analysis aborted: " + t.getMessage());
@@ -229,13 +227,13 @@ else if (cmd.hasOption(CliArgs.ARG_LOG_DIR)) {
229227
}
230228

231229
// path to Android SDK jar
232-
if (checkRequiredUse(cmd, CliArgs.ARG_ANDROID_LIB, OpMode.PROFILE, OpMode.MATCH)) {
230+
if (checkRequiredUse(cmd, CliArgs.ARG_ANDROID_LIB, OpMode.PROFILE, OpMode.MATCH, OpMode.LIB_API_ANALYSIS)) {
233231
CliOptions.pathToAndroidJar = new File(cmd.getOptionValue(CliArgs.ARG_ANDROID_LIB));
234232
}
235233

236234

237235
// profiles dir option, if provided without argument output is written to default dir
238-
if (checkOptionalUse(cmd, CliArgs.ARG_PROFILES_DIR, OpMode.PROFILE, OpMode.MATCH, OpMode.DB, OpMode.LIB_API_ANALYSIS)) {
236+
if (checkOptionalUse(cmd, CliArgs.ARG_PROFILES_DIR, OpMode.PROFILE, OpMode.MATCH, OpMode.DB)) {
239237
File profilesDir = new File(cmd.getOptionValue(CliArgs.ARG_PROFILES_DIR));
240238
if (profilesDir.exists() && !profilesDir.isDirectory())
241239
throw new ParseException("Profiles directory " + profilesDir + " already exists and is not a directory");
@@ -300,34 +298,49 @@ else if (cmd.hasOption(CliArgs.ARG_LOG_DIR)) {
300298
* - in profile mode pass *one* library (since it is linked to lib description file)
301299
* - in match mode pass one application file or one directory file (including apks)
302300
*/
303-
if (!(CliOptions.opmode.equals(OpMode.DB) || CliOptions.opmode.equals(OpMode.LIB_API_ANALYSIS))) {
301+
if (!(CliOptions.opmode.equals(OpMode.DB))) {
304302
inputFiles = new ArrayList<File>();
305-
String[] fileExts = CliOptions.opmode.equals(OpMode.MATCH)? new String[]{"apk"} : new String[]{"jar", "aar"};
306-
307-
for (String apkFileName: cmd.getArgs()) {
308-
File arg = new File(apkFileName);
309-
310-
if (arg.isDirectory()) {
311-
inputFiles.addAll(Utils.collectFiles(arg, fileExts));
312-
} else if (arg.isFile()) {
313-
if (arg.getName().endsWith("." + fileExts[0]))
314-
inputFiles.add(arg);
315-
else if (fileExts.length > 1 && arg.getName().endsWith("." + fileExts[1]))
316-
inputFiles.add(arg);
317-
else
318-
throw new ParseException("File " + arg.getName() + " is no valid ." + Utils.join(Arrays.asList(fileExts), "/") + " file");
319-
} else {
320-
throw new ParseException("Argument is no valid file or directory!");
303+
304+
if (CliOptions.opmode.equals(OpMode.LIB_API_ANALYSIS)) {
305+
// we require a directory including library packages/descriptions
306+
for (String path: cmd.getArgs()) {
307+
File dir = new File(path);
308+
309+
if (dir.isDirectory())
310+
inputFiles.add(dir);
311+
}
312+
313+
if (inputFiles.isEmpty()) {
314+
throw new ParseException("You have to provide at least one directory that includes a library package and description");
321315
}
316+
} else {
317+
String[] fileExts = CliOptions.opmode.equals(OpMode.MATCH) ? new String[]{"apk"} : new String[]{"jar", "aar"};
318+
319+
for (String apkFileName : cmd.getArgs()) {
320+
File arg = new File(apkFileName);
321+
322+
if (arg.isDirectory()) {
323+
inputFiles.addAll(Utils.collectFiles(arg, fileExts));
324+
} else if (arg.isFile()) {
325+
if (arg.getName().endsWith("." + fileExts[0]))
326+
inputFiles.add(arg);
327+
else if (fileExts.length > 1 && arg.getName().endsWith("." + fileExts[1]))
328+
inputFiles.add(arg);
329+
else
330+
throw new ParseException("File " + arg.getName() + " is no valid ." + Utils.join(Arrays.asList(fileExts), "/") + " file");
331+
} else {
332+
throw new ParseException("Argument is no valid file or directory!");
333+
}
334+
}
335+
336+
if (inputFiles.isEmpty()) {
337+
if (CliOptions.opmode.equals(OpMode.PROFILE))
338+
throw new ParseException("You have to provide one library.jar to be processed");
339+
else
340+
throw new ParseException("You have to provide a path to a single application file or a directory");
341+
} else if (inputFiles.size() > 1 && CliOptions.opmode.equals(OpMode.PROFILE))
342+
throw new ParseException("You have to provide a path to a single library file or a directory incl. a single lib file");
322343
}
323-
324-
if (inputFiles.isEmpty()) {
325-
if (CliOptions.opmode.equals(OpMode.PROFILE))
326-
throw new ParseException("You have to provide one library.jar to be processed");
327-
else
328-
throw new ParseException("You have to provide a path to a single application file or a directory");
329-
} else if (inputFiles.size() > 1 && CliOptions.opmode.equals(OpMode.PROFILE))
330-
throw new ParseException("You have to provide a path to a single library file or a directory incl. a single lib file");
331344
}
332345

333346
} catch (ParseException e) {

src/de/infsec/tpl/eval/LibApiRobustnessStats.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
* - # public APIs per version
3333
* - APIs -> set of versions in which they are included
3434
* - set of alternative APIs (if any), if API is no longer in some version
35+
*
36+
* @deprecated
3537
*/
3638

3739
public class LibApiRobustnessStats implements Serializable {

src/de/infsec/tpl/eval/LibraryApiAnalysis.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
* are public APIs removed, modified, and/or added
4545
* For each library a {@link LibApiRobustnessStats} is created that tracks APIs across versions
4646
* The results are written in the configured json directory and logged.
47+
*
48+
* @deprecated
4749
*/
4850

4951
public class LibraryApiAnalysis {
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package de.infsec.tpl.modules.libapi;
2+
3+
import com.ibm.wala.classLoader.IMethod;
4+
import de.infsec.tpl.utils.Utils;
5+
6+
import java.util.ArrayList;
7+
import java.util.HashMap;
8+
import java.util.List;
9+
10+
/**
11+
* Spec taken from https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html
12+
*/
13+
public enum JvmMethodAccessFlags {
14+
15+
NO_FLAG (0x0000, "no-flag"),
16+
PUBLIC (0x0001, "public"),
17+
PRIVATE (0x0002, "private"),
18+
PROTECTED (0x0004, "protected"),
19+
STATIC (0x0008, "static"),
20+
FINAL (0x0010, "final"),
21+
ABSTRACT (0x0400, "abstract"),
22+
PACKAGE_PROTECTED (0x2000, "package-proteced");
23+
24+
private int value;
25+
private String accessFlagName;
26+
27+
//cache the array of all AccessFlags, because .values() allocates a new array for every call
28+
private final static JvmMethodAccessFlags[] allFlags;
29+
30+
private final static List<Integer> validFlagValues;
31+
32+
private static HashMap<String, JvmMethodAccessFlags> accessFlagsByName;
33+
34+
static {
35+
allFlags = JvmMethodAccessFlags.values();
36+
37+
validFlagValues = new ArrayList<Integer>();
38+
for (JvmMethodAccessFlags flag: allFlags)
39+
validFlagValues.add(flag.getValue());
40+
41+
accessFlagsByName = new HashMap<String, JvmMethodAccessFlags>();
42+
for (JvmMethodAccessFlags accessFlag: allFlags) {
43+
accessFlagsByName.put(accessFlag.accessFlagName, accessFlag);
44+
}
45+
}
46+
47+
private JvmMethodAccessFlags(int value, String accessFlagName) {
48+
this.value = value;
49+
this.accessFlagName = accessFlagName;
50+
}
51+
52+
53+
private static String flags2Str(JvmMethodAccessFlags[] accessFlags) {
54+
int size = 0;
55+
for (JvmMethodAccessFlags accessFlag: accessFlags) {
56+
size += accessFlag.toString().length() + 1;
57+
}
58+
59+
StringBuilder sb = new StringBuilder(size);
60+
for (JvmMethodAccessFlags accessFlag: accessFlags) {
61+
sb.append(accessFlag.toString());
62+
sb.append(" ");
63+
}
64+
if (accessFlags.length > 0) {
65+
sb.delete(sb.length() - 1, sb.length());
66+
}
67+
return sb.toString();
68+
}
69+
70+
public static boolean isValidFlag(int code) {
71+
return validFlagValues.contains(code);
72+
}
73+
74+
75+
public static String flags2Str(int code) {
76+
List<String> matchedFlags = new ArrayList<String>();
77+
78+
for (JvmMethodAccessFlags flag: allFlags) {
79+
if ((code & flag.value) != 0x0) {
80+
matchedFlags.add(flag.accessFlagName + "(" + flag.value + ")");
81+
}
82+
}
83+
84+
return Utils.join(matchedFlags, ",");
85+
}
86+
87+
public static String flags2Str(IMethod m) {
88+
return flags2Str(getMethodAccessCode(m));
89+
}
90+
91+
public static JvmMethodAccessFlags getAccessFlag(String accessFlag) {
92+
return accessFlagsByName.get(accessFlag);
93+
}
94+
95+
public int getValue() {
96+
return value;
97+
}
98+
99+
public String toString() {
100+
return accessFlagName;
101+
}
102+
103+
104+
public static int getAccessFlagFilter(JvmMethodAccessFlags... flags) {
105+
int filter = NO_FLAG.getValue();
106+
107+
if (flags != null) {
108+
for (JvmMethodAccessFlags flag: flags) {
109+
if (!JvmMethodAccessFlags.isValidFlag(flag.getValue())) continue;
110+
111+
filter |= flag.getValue();
112+
}
113+
}
114+
115+
return filter;
116+
}
117+
118+
public static int getPublicOnlyFilter() {
119+
return getAccessFlagFilter(JvmMethodAccessFlags.PRIVATE, JvmMethodAccessFlags.PROTECTED, JvmMethodAccessFlags.STATIC,
120+
JvmMethodAccessFlags.FINAL, JvmMethodAccessFlags.ABSTRACT, JvmMethodAccessFlags.PACKAGE_PROTECTED);
121+
}
122+
123+
124+
public static int getMethodAccessCode(IMethod m) {
125+
int res = 0x0;
126+
127+
if (m == null)
128+
return res;
129+
130+
if (m.isPublic()) {
131+
res |= JvmMethodAccessFlags.PUBLIC.getValue();
132+
} else if (m.isProtected()) {
133+
res |= JvmMethodAccessFlags.PROTECTED.getValue();
134+
} else if (m.isPrivate()) {
135+
res |= JvmMethodAccessFlags.PRIVATE.getValue();
136+
} else {
137+
res |= JvmMethodAccessFlags.PACKAGE_PROTECTED.getValue();
138+
}
139+
140+
if (m.isStatic())
141+
res |= JvmMethodAccessFlags.STATIC.getValue();
142+
if (m.isFinal())
143+
res |= JvmMethodAccessFlags.FINAL.getValue();
144+
if (m.isAbstract())
145+
res |= JvmMethodAccessFlags.ABSTRACT.getValue();
146+
147+
return res;
148+
}
149+
150+
}

0 commit comments

Comments
 (0)