Skip to content
This repository was archived by the owner on Aug 28, 2025. It is now read-only.

Commit efa99cf

Browse files
authored
Merge pull request #16 from lsd-cat/codex/add-api-for-module-execution-and-documentation
Add Java AndroidQF API
2 parents 0784e1e + 920ac0c commit efa99cf

File tree

3 files changed

+199
-0
lines changed

3 files changed

+199
-0
lines changed

java/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ gradle wrapper
1414
./gradlew test
1515
```
1616

17+
## AndroidQF API
18+
The `AndroidQFRunner` class can parse a directory exported with the
19+
[androidqf](https://mvt.re/) format and run all available modules.
20+
Example:
21+
```java
22+
Path dir = Path.of("/path/to/androidqf");
23+
Indicators iocs = Indicators.loadFromDirectory(Path.of("/path/to/iocs").toFile());
24+
AndroidQFRunner runner = new AndroidQFRunner(dir);
25+
runner.setIndicators(iocs);
26+
Map<String, Artifact> result = runner.runAll();
27+
```
28+
29+
Individual modules can be invoked via `runModule("processes")` etc.
30+
See `AndroidQFRunner.AVAILABLE_MODULES` for the list of names.
31+
32+
1733
## Next steps
1834
- Translate more artifact parsers from Python.
1935
- Extend Detection metadata (source file, STIX IDs, etc.).
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package org.osservatorionessuno.libmvt.android;
2+
3+
import org.osservatorionessuno.libmvt.android.artifacts.*;
4+
import org.osservatorionessuno.libmvt.common.Artifact;
5+
import org.osservatorionessuno.libmvt.common.Indicators;
6+
7+
import java.io.IOException;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.util.*;
11+
12+
/**
13+
* Simple helper to run the available AndroidQF artifact parsers on a folder
14+
* containing extracted androidqf data.
15+
*/
16+
public class AndroidQFRunner {
17+
private final Path directory;
18+
private Indicators indicators;
19+
20+
public AndroidQFRunner(Path directory) {
21+
this.directory = directory;
22+
}
23+
24+
/** Assign indicators to use for IOC matching. */
25+
public void setIndicators(Indicators indicators) {
26+
this.indicators = indicators;
27+
}
28+
29+
/** Run all known modules on the provided directory. */
30+
public Map<String, Artifact> runAll() throws Exception {
31+
Map<String, Artifact> map = new LinkedHashMap<>();
32+
for (String name : AVAILABLE_MODULES) {
33+
Artifact art = runModule(name);
34+
if (art != null) {
35+
map.put(name, art);
36+
}
37+
}
38+
return map;
39+
}
40+
41+
/** Run a single module by name. */
42+
public Artifact runModule(String moduleName) throws Exception {
43+
return runModule(moduleName, this.directory);
44+
}
45+
46+
/** Run a single module on a custom directory. */
47+
public Artifact runModule(String moduleName, Path dir) throws Exception {
48+
return switch (moduleName) {
49+
case "dumpsys_accessibility" -> runDumpsysSection(dir, new DumpsysAccessibility(),
50+
"DUMP OF SERVICE accessibility:");
51+
case "dumpsys_activities" -> runDumpsysSection(dir, new DumpsysPackageActivities(),
52+
"DUMP OF SERVICE package:");
53+
case "dumpsys_receivers" -> runDumpsysSection(dir, new DumpsysReceivers(),
54+
"DUMP OF SERVICE package:");
55+
case "dumpsys_adb" -> runDumpsysSection(dir, new DumpsysAdb(),
56+
"DUMP OF SERVICE adb:");
57+
case "dumpsys_appops" -> runDumpsysSection(dir, new DumpsysAppops(),
58+
"DUMP OF SERVICE appops:");
59+
case "dumpsys_battery_daily" -> runDumpsysSection(dir, new DumpsysBatteryDaily(),
60+
"DUMP OF SERVICE batterystats:");
61+
case "dumpsys_battery_history" -> runDumpsysSection(dir, new DumpsysBatteryHistory(),
62+
"DUMP OF SERVICE batterystats:");
63+
case "dumpsys_dbinfo" -> runDumpsysSection(dir, new DumpsysDBInfo(),
64+
"DUMP OF SERVICE dbinfo:");
65+
case "dumpsys_packages" -> runDumpsysSection(dir, new DumpsysPackages(),
66+
"DUMP OF SERVICE package:");
67+
case "dumpsys_platform_compat" -> runDumpsysSection(dir, new DumpsysPlatformCompat(),
68+
"DUMP OF SERVICE platform_compat:");
69+
case "processes" -> runSimpleFile(dir, "ps.txt", new Processes());
70+
case "getprop" -> runSimpleFile(dir, "getprop.txt", new GetProp());
71+
case "settings" -> runSettings(dir);
72+
default -> throw new IllegalArgumentException("Unknown module: " + moduleName);
73+
};
74+
}
75+
76+
private Artifact finalizeArtifact(AndroidArtifact art) {
77+
if (indicators != null) {
78+
art.setIndicators(indicators);
79+
art.checkIndicators();
80+
}
81+
return art;
82+
}
83+
84+
private Artifact runDumpsysSection(Path dir, AndroidArtifact art, String header) throws Exception {
85+
Path file = dir.resolve("dumpsys.txt");
86+
if (!Files.exists(file)) return null;
87+
String dumpsys = Files.readString(file);
88+
String section = extractSection(dumpsys, header);
89+
art.parse(section);
90+
return finalizeArtifact(art);
91+
}
92+
93+
private Artifact runSimpleFile(Path dir, String name, AndroidArtifact art) throws Exception {
94+
Path file = dir.resolve(name);
95+
if (!Files.exists(file)) return null;
96+
String data = Files.readString(file);
97+
art.parse(data);
98+
return finalizeArtifact(art);
99+
}
100+
101+
private Artifact runSettings(Path dir) throws Exception {
102+
List<Path> files;
103+
try (var stream = Files.list(dir)) {
104+
files = stream.filter(p -> p.getFileName().toString().startsWith("settings_")
105+
&& p.getFileName().toString().endsWith(".txt")).toList();
106+
}
107+
if (files.isEmpty()) return null;
108+
StringBuilder sb = new StringBuilder();
109+
for (Path f : files) {
110+
sb.append(Files.readString(f)).append("\n");
111+
}
112+
Settings settings = new Settings();
113+
settings.parse(sb.toString());
114+
return finalizeArtifact(settings);
115+
}
116+
117+
private static String extractSection(String dumpsys, String header) {
118+
List<String> lines = new ArrayList<>();
119+
boolean inSection = false;
120+
String delimiter = "-".repeat(78);
121+
for (String line : dumpsys.split("\n")) {
122+
if (line.trim().equals(header)) {
123+
inSection = true;
124+
continue;
125+
}
126+
if (!inSection) continue;
127+
if (line.trim().startsWith(delimiter)) break;
128+
lines.add(line);
129+
}
130+
return String.join("\n", lines);
131+
}
132+
133+
/** List of all module names understood by the runner. */
134+
public static final List<String> AVAILABLE_MODULES = List.of(
135+
"dumpsys_accessibility",
136+
"dumpsys_activities",
137+
"dumpsys_receivers",
138+
"dumpsys_adb",
139+
"dumpsys_appops",
140+
"dumpsys_battery_daily",
141+
"dumpsys_battery_history",
142+
"dumpsys_dbinfo",
143+
"dumpsys_packages",
144+
"dumpsys_platform_compat",
145+
"processes",
146+
"getprop",
147+
"settings"
148+
);
149+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.osservatorionessuno.libmvt.android;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.osservatorionessuno.libmvt.common.Artifact;
5+
import org.osservatorionessuno.libmvt.common.Indicators;
6+
7+
import java.nio.file.Path;
8+
import java.util.Map;
9+
10+
import static org.junit.jupiter.api.Assertions.*;
11+
12+
public class AndroidQFRunnerTest {
13+
@Test
14+
public void testRunAllModules() throws Exception {
15+
Path dir = Path.of("src", "test", "resources", "androidqf");
16+
Indicators ind = Indicators.loadFromDirectory(Path.of("src","test","resources","iocs").toFile());
17+
AndroidQFRunner runner = new AndroidQFRunner(dir);
18+
runner.setIndicators(ind);
19+
Map<String, Artifact> res = runner.runAll();
20+
assertTrue(res.containsKey("processes"));
21+
Artifact proc = res.get("processes");
22+
assertEquals(15, proc.getResults().size());
23+
assertTrue(res.containsKey("getprop"));
24+
assertEquals(10, res.get("getprop").getResults().size());
25+
}
26+
27+
@Test
28+
public void testRunSingleModule() throws Exception {
29+
Path dir = Path.of("src", "test", "resources", "androidqf");
30+
AndroidQFRunner runner = new AndroidQFRunner(dir);
31+
Artifact art = runner.runModule("getprop");
32+
assertEquals(10, art.getResults().size());
33+
}
34+
}

0 commit comments

Comments
 (0)