frida-test 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -29
- package/bin/frida-test-compile.js +1 -1
- package/bin/frida-test.js +1 -1
- package/dist/host/bundler.d.ts.map +1 -0
- package/dist/{bundler.js → host/bundler.js} +12 -14
- package/dist/host/cli-frida-test-compiler.d.ts.map +1 -0
- package/dist/{cli-frida-test-compiler.js → host/cli-frida-test-compiler.js} +2 -2
- package/dist/host/cli-frida-test.d.ts.map +1 -0
- package/dist/{cli-frida-test.js → host/cli-frida-test.js} +10 -4
- package/dist/host/collector.d.ts.map +1 -0
- package/dist/host/collector.js +40 -0
- package/dist/host/device.d.ts +12 -0
- package/dist/host/device.d.ts.map +1 -0
- package/dist/host/device.js +19 -0
- package/dist/{logger.d.ts → host/logger.d.ts} +0 -2
- package/dist/host/logger.d.ts.map +1 -0
- package/dist/{logger.js → host/logger.js} +1 -3
- package/dist/host/protocol.d.ts.map +1 -0
- package/dist/host/protocol.js +12 -0
- package/dist/host/reporter/console.d.ts +4 -0
- package/dist/host/reporter/console.d.ts.map +1 -0
- package/dist/{reporter → host/reporter}/console.js +5 -9
- package/dist/host/reporter/json.d.ts.map +1 -0
- package/dist/host/reporter/summary.d.ts.map +1 -0
- package/dist/{reporter → host/reporter}/summary.js +6 -5
- package/dist/host/runner.d.ts.map +1 -0
- package/dist/{runner.js → host/runner.js} +20 -10
- package/dist/host/target.d.ts.map +1 -0
- package/dist/{target.js → host/target.js} +11 -6
- package/package.json +14 -8
- package/src/agent-runtime/expect.ts +5 -0
- package/src/agent-runtime/fridaTest.ts +9 -0
- package/src/agent-runtime/globals.d.ts +8 -4
- package/src/agent-runtime/globals.ts +10 -4
- package/src/agent-runtime/matchers.ts +102 -119
- package/src/agent-runtime/mock.ts +131 -0
- package/src/agent-runtime/modifiers.ts +102 -0
- package/src/agent-runtime/registry.ts +14 -3
- package/src/agent-runtime/spy.ts +30 -0
- package/dist/bundler.d.ts.map +0 -1
- package/dist/cli-frida-test-compiler.d.ts.map +0 -1
- package/dist/cli-frida-test.d.ts.map +0 -1
- package/dist/collector.d.ts.map +0 -1
- package/dist/collector.js +0 -27
- package/dist/device.d.ts +0 -28
- package/dist/device.d.ts.map +0 -1
- package/dist/device.js +0 -68
- package/dist/logger.d.ts.map +0 -1
- package/dist/protocol.d.ts.map +0 -1
- package/dist/protocol.js +0 -6
- package/dist/reporter/console.d.ts +0 -3
- package/dist/reporter/console.d.ts.map +0 -1
- package/dist/reporter/json.d.ts.map +0 -1
- package/dist/reporter/summary.d.ts.map +0 -1
- package/dist/runner.d.ts.map +0 -1
- package/dist/target.d.ts.map +0 -1
- /package/dist/{bundler.d.ts → host/bundler.d.ts} +0 -0
- /package/dist/{cli-frida-test-compiler.d.ts → host/cli-frida-test-compiler.d.ts} +0 -0
- /package/dist/{cli-frida-test.d.ts → host/cli-frida-test.d.ts} +0 -0
- /package/dist/{collector.d.ts → host/collector.d.ts} +0 -0
- /package/dist/{protocol.d.ts → host/protocol.d.ts} +0 -0
- /package/dist/{reporter → host/reporter}/json.d.ts +0 -0
- /package/dist/{reporter → host/reporter}/json.js +0 -0
- /package/dist/{reporter → host/reporter}/summary.d.ts +0 -0
- /package/dist/{runner.d.ts → host/runner.d.ts} +0 -0
- /package/dist/{target.d.ts → host/target.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frida-test Documentation
|
|
2
2
|
|
|
3
|
+
[](https://github.com/bernhste/frida-test/actions/workflows/test.yml)
|
|
4
|
+
|
|
3
5
|
This is a small test framework which runs on the target. It is used to unit test Frida code running on actual devices. It was originally developed to test the Frida agent code used in [frooky](https://github.com/cpholguera/frooky).
|
|
4
6
|
|
|
5
7
|
The following chapters explain how to write and run tests.
|
|
@@ -10,7 +12,7 @@ The following chapters explain how to write and run tests.
|
|
|
10
12
|
npm install --save-dev frida-test
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
After the installation, import the type `frida-test` into
|
|
15
|
+
After the installation, import the type `frida-test` into your project by adding the following configuration to the `tsconfig.json`:
|
|
14
16
|
|
|
15
17
|
```json
|
|
16
18
|
{
|
|
@@ -42,28 +44,9 @@ describe('Classloader', () => {
|
|
|
42
44
|
|
|
43
45
|
Tests can be nested to any depth and can be synchronous or asynchronous.
|
|
44
46
|
|
|
45
|
-
### Test discovery
|
|
46
|
-
|
|
47
|
-
The framework collects every file matching `*.test.ts` in the directories passed on the command line, recursively.
|
|
48
|
-
|
|
49
|
-
A good practice is to create test files next to the source code file as shown here:
|
|
50
|
-
|
|
51
|
-
```text
|
|
52
|
-
myProject/
|
|
53
|
-
├── android/
|
|
54
|
-
│ ├── classLoader.ts
|
|
55
|
-
│ └── classLoader.test.ts
|
|
56
|
-
├── ios/
|
|
57
|
-
│ ├── objcRuntime.ts
|
|
58
|
-
│ └── objcRuntime.test.ts
|
|
59
|
-
└── shared/
|
|
60
|
-
├── helper.ts
|
|
61
|
-
└── helper.test.ts
|
|
62
|
-
```
|
|
63
|
-
|
|
64
47
|
## Matchers
|
|
65
48
|
|
|
66
|
-
`expect(actualValue)` returns a `
|
|
49
|
+
`expect(actualValue)` returns a `Matchers` object which we can use to test for the expected value. Use the following functions to do that:
|
|
67
50
|
|
|
68
51
|
| Matcher | Description |
|
|
69
52
|
| --- | --- |
|
|
@@ -76,15 +59,73 @@ myProject/
|
|
|
76
59
|
| `.toBeUndefined()` | Value is `undefined` |
|
|
77
60
|
| `.toBeGreaterThan(value)` | Numeric value is greater than `value` |
|
|
78
61
|
| `.toBeLessThan(value)` | Numeric value is less than `value` |
|
|
79
|
-
| `.toContain(value)` |
|
|
80
|
-
| `.
|
|
81
|
-
| `.
|
|
82
|
-
| `.toHaveBeenCalled()` |
|
|
83
|
-
| `.
|
|
84
|
-
| `.
|
|
62
|
+
| `.toContain(value)` | Array, `Set`, or string contains `value` (array/`Set` items compared with `===`) |
|
|
63
|
+
| `.toContainEqual(value)` | Array or `Set` contains an item deeply equal to `value` |
|
|
64
|
+
| `.toThrow(errorMatch?)` | Function throws; `errorMatch` can be a substring, a `RegExp` matched against the message, an `Error` instance (message equality only), or an `Error` class (`instanceof` check) |
|
|
65
|
+
| `.toHaveBeenCalled()` | Mock/spy was called at least once |
|
|
66
|
+
| `.toHaveBeenCalledTimes(count)` | Mock/spy was called exactly `count` times |
|
|
67
|
+
| `.toHaveBeenCalledWith(...expected)` | Mock/spy was called (at any point) with the expected arguments |
|
|
68
|
+
| `.toHaveBeenLastCalledWith(...expected)` | Mock/spy's most recent call had the expected arguments |
|
|
69
|
+
| `.toHaveBeenNthCalledWith(n, ...expected)` | Mock/spy's `n`th call (1-indexed) had the expected arguments |
|
|
85
70
|
|
|
86
71
|
> [!NOTE]
|
|
87
|
-
> `frida-test`
|
|
72
|
+
> `frida-test` tests itself. So for examples of all matchers and more, have a look at the `*.test.ts` files located in the [test folder](./tests/).
|
|
73
|
+
|
|
74
|
+
### Modifiers
|
|
75
|
+
|
|
76
|
+
| Modifier | Description |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| `.not` | Inverts the assertion result, e.g. `expect(2 + 2).not.toBe(5)` |
|
|
79
|
+
| `.resolves` | Unwraps a resolved promise so a matcher applies to its value; the assertion must be `await`ed. The received value must actually be a `Promise` - it fails otherwise |
|
|
80
|
+
| `.rejects` | Unwraps a rejected promise so a matcher applies to its reason; the assertion must be `await`ed. The received value must actually be a `Promise` - it fails otherwise |
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
await expect(fetchUser(1)).resolves.toEqual({ id: 1, name: 'Ada' });
|
|
84
|
+
await expect(fetchUser(-1)).rejects.toThrow('not found');
|
|
85
|
+
|
|
86
|
+
// modifiers compose:
|
|
87
|
+
await expect(fetchUser(1)).resolves.not.toBeNull();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Mocking
|
|
91
|
+
|
|
92
|
+
`frida-test` mocks and spies follow the same API shape as Jest's mock functions.
|
|
93
|
+
|
|
94
|
+
- `fn(implementation?)`: creates a standalone mock function, optionally backed by `implementation`.
|
|
95
|
+
- `spyOn(object, methodName)`: replaces `object[methodName]` with a mock that calls through to the original method by default, and can be restored later.
|
|
96
|
+
|
|
97
|
+
Both forms return the same `Mock` type:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const mock = fridaTest.fn((a: number, b: number) => a + b);
|
|
101
|
+
mock(1, 2);
|
|
102
|
+
|
|
103
|
+
expect(mock).toHaveBeenCalledWith(1, 2);
|
|
104
|
+
mock.mock.calls; // [[1, 2]]
|
|
105
|
+
mock.mock.results; // [{ type: "return", value: 3 }]
|
|
106
|
+
|
|
107
|
+
mock.mockReturnValue(42); // set a default return value
|
|
108
|
+
mock.mockReturnValueOnce(99); // ...for just the next call
|
|
109
|
+
mock.mockResolvedValue(value); // wraps value in Promise.resolve()
|
|
110
|
+
mock.mockRejectedValue(error); // wraps error in Promise.reject()
|
|
111
|
+
mock.mockImplementation(impl); // replace the implementation
|
|
112
|
+
mock.mockImplementationOnce(impl); // ...for just the next call
|
|
113
|
+
|
|
114
|
+
mock.mockClear(); // reset calls/results, keep the implementation
|
|
115
|
+
mock.mockReset(); // reset calls/results and drop the implementation
|
|
116
|
+
mock.mockRestore(); // reset like mockReset(); for spyOn(), also restores the original method
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
describe('Logger', () => {
|
|
121
|
+
it('should call the underlying console method', () => {
|
|
122
|
+
const spy = fridaTest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
123
|
+
logMessage('hello');
|
|
124
|
+
expect(spy).toHaveBeenCalledWith('hello');
|
|
125
|
+
spy.mockRestore();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
```
|
|
88
129
|
|
|
89
130
|
### Setup and Teardown
|
|
90
131
|
|
|
@@ -169,7 +210,7 @@ frida-test -U -N org.owasp.mastestapp.MASTestApp-iOS ./tests/ios ./tests/shared
|
|
|
169
210
|
frida-test -H 192.168.1.10:27042 --token secret -p 4926 ./tests/shared
|
|
170
211
|
```
|
|
171
212
|
|
|
172
|
-
##
|
|
213
|
+
## Compiling the Agent
|
|
173
214
|
|
|
174
215
|
`frida-test` automatically bundles the test suites and compiles them together with the testing framework into a Frida agent.
|
|
175
216
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import("../dist/cli-frida-test-compiler.js").catch((err) => {
|
|
3
|
+
import("../dist/host/cli-frida-test-compiler.js").catch((err) => {
|
|
4
4
|
console.error("Failed to load the CLI module. Make sure the project is built.");
|
|
5
5
|
console.error(err);
|
|
6
6
|
process.exit(1);
|
package/bin/frida-test.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundler.d.ts","sourceRoot":"","sources":["../../src/host/bundler.ts"],"names":[],"mappings":"AAyFA,wBAAsB,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,OAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CA2DlG"}
|
|
@@ -8,29 +8,28 @@ const IMPORT_MARKER = "/// IMPORT TESTS SUITES ///";
|
|
|
8
8
|
const AGENT_ENTRYPOINT_FILENAME = "agentRuntime.ts";
|
|
9
9
|
const AGENT_ENTRYPOINT_BASENAME = path.parse(AGENT_ENTRYPOINT_FILENAME).name;
|
|
10
10
|
const AGENT_BUNDLE_FILENAME = `${AGENT_ENTRYPOINT_BASENAME}.bundle.js`;
|
|
11
|
-
function
|
|
12
|
-
const startDir = process.cwd();
|
|
11
|
+
function findPackageRoot(startDir) {
|
|
13
12
|
let dir = startDir;
|
|
14
13
|
while (true) {
|
|
15
14
|
if (existsSync(path.join(dir, "package.json")))
|
|
16
15
|
return dir;
|
|
17
16
|
const parent = path.dirname(dir);
|
|
18
17
|
if (parent === dir)
|
|
19
|
-
return
|
|
18
|
+
return undefined;
|
|
20
19
|
dir = parent;
|
|
21
20
|
}
|
|
22
21
|
}
|
|
22
|
+
function getProjectRoot() {
|
|
23
|
+
const startDir = process.cwd();
|
|
24
|
+
return findPackageRoot(startDir) ?? startDir;
|
|
25
|
+
}
|
|
23
26
|
function getPackageRoot() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const parent = path.dirname(dir);
|
|
29
|
-
if (parent === dir) {
|
|
30
|
-
throw new Error("Could not locate frida-test's own package.json.");
|
|
31
|
-
}
|
|
32
|
-
dir = parent;
|
|
27
|
+
const startDir = path.dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const root = findPackageRoot(startDir);
|
|
29
|
+
if (!root) {
|
|
30
|
+
throw new Error("Could not locate frida-test's own package.json.");
|
|
33
31
|
}
|
|
32
|
+
return root;
|
|
34
33
|
}
|
|
35
34
|
function getAgentRuntimeSrcDir() {
|
|
36
35
|
const dir = path.join(getPackageRoot(), "src", "agent-runtime");
|
|
@@ -99,7 +98,6 @@ export async function bundleAgent(testSuitePaths, keep = false) {
|
|
|
99
98
|
return `import ${JSON.stringify(specifier)};`;
|
|
100
99
|
})
|
|
101
100
|
.join("\n");
|
|
102
|
-
await rm(entrypointPath, { force: true });
|
|
103
101
|
await writeFile(entrypointPath, agentSource.replace(IMPORT_MARKER, importStatements), "utf8");
|
|
104
102
|
await prepareTypeCheckConfig(projectRoot, workDir);
|
|
105
103
|
const outfilePath = path.join(workDir, AGENT_BUNDLE_FILENAME);
|
|
@@ -115,7 +113,7 @@ export async function bundleAgent(testSuitePaths, keep = false) {
|
|
|
115
113
|
catch (err) {
|
|
116
114
|
throw new Error(`frida-compile failed for entrypoint "${entrypointPath}" with suites [${testSuitePaths.join(", ")}] (see compiler output above for details)`, { cause: err });
|
|
117
115
|
}
|
|
118
|
-
logger.info(`Agent bundle
|
|
116
|
+
logger.info(`Agent bundle successfully created and saved at ${outfilePath}.`);
|
|
119
117
|
return await readFile(outfilePath, "utf8");
|
|
120
118
|
}
|
|
121
119
|
finally {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-frida-test-compiler.d.ts","sourceRoot":"","sources":["../../src/host/cli-frida-test-compiler.ts"],"names":[],"mappings":""}
|
|
@@ -5,10 +5,10 @@ import { bundleAgent } from "./bundler.js";
|
|
|
5
5
|
import { collectTestSuitePaths } from "./collector.js";
|
|
6
6
|
import { logger } from "./logger.js";
|
|
7
7
|
const usage = `
|
|
8
|
-
Usage: frida-test-
|
|
8
|
+
Usage: frida-test-compile [options] <src_path>...
|
|
9
9
|
|
|
10
10
|
Options:
|
|
11
|
-
-o, --out <path> Path of the output file for
|
|
11
|
+
-o, --out <path> Path of the output file for the compiled agent bundle (default: stdout)
|
|
12
12
|
-h, --help Show this help message
|
|
13
13
|
`;
|
|
14
14
|
class CliError extends Error {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-frida-test.d.ts","sourceRoot":"","sources":["../../src/host/cli-frida-test.ts"],"names":[],"mappings":""}
|
|
@@ -3,11 +3,12 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { bundleAgent } from "./bundler.js";
|
|
5
5
|
import { collectTestSuitePaths } from "./collector.js";
|
|
6
|
-
import { resolveDevice
|
|
6
|
+
import { resolveDevice } from "./device.js";
|
|
7
7
|
import { logger } from "./logger.js";
|
|
8
8
|
import { writeRunSummaryJson } from "./reporter/json.js";
|
|
9
9
|
import { printSummary } from "./reporter/summary.js";
|
|
10
10
|
import { TestRunner } from "./runner.js";
|
|
11
|
+
import { resolveTarget } from "./target.js";
|
|
11
12
|
const usage = `
|
|
12
13
|
Usage: frida-test [options] <src_path>...
|
|
13
14
|
|
|
@@ -28,7 +29,7 @@ const usage = `
|
|
|
28
29
|
-o, --out <path> Path of the output file for JSON reporter (default: disabled)
|
|
29
30
|
-t, --timeout <s> Abort the run after this many seconds (default: 600, 0 disables)
|
|
30
31
|
-d, --delay <s> Start running the test suites after this many seconds (default: 0)
|
|
31
|
-
-k, --keep Keep the generated agent in .frida-test/
|
|
32
|
+
-k, --keep Keep the generated agent bundle in .frida-test-cache/
|
|
32
33
|
-v, --verbose Enable verbose logging
|
|
33
34
|
-h, --help Show this help message
|
|
34
35
|
|
|
@@ -106,7 +107,7 @@ async function main() {
|
|
|
106
107
|
else if (values.usb) {
|
|
107
108
|
deviceSelector = "usb";
|
|
108
109
|
}
|
|
109
|
-
else if (values.host !== undefined ||
|
|
110
|
+
else if (values.host !== undefined || hasRemoteParams) {
|
|
110
111
|
const keepaliveInterval = values["keepalive-interval"] !== undefined ? parseKeepaliveInterval(values["keepalive-interval"]) : undefined;
|
|
111
112
|
deviceSelector = {
|
|
112
113
|
host: values.host ?? "127.0.0.1:27042",
|
|
@@ -116,6 +117,9 @@ async function main() {
|
|
|
116
117
|
...(keepaliveInterval !== undefined && { keepaliveInterval }),
|
|
117
118
|
};
|
|
118
119
|
}
|
|
120
|
+
else if (values.remote) {
|
|
121
|
+
deviceSelector = "remote";
|
|
122
|
+
}
|
|
119
123
|
else {
|
|
120
124
|
deviceSelector = "local";
|
|
121
125
|
}
|
|
@@ -179,7 +183,9 @@ async function main() {
|
|
|
179
183
|
});
|
|
180
184
|
let runSummary;
|
|
181
185
|
try {
|
|
182
|
-
|
|
186
|
+
const testsPromise = runner.runTests();
|
|
187
|
+
testsPromise.catch(() => { });
|
|
188
|
+
runSummary = await Promise.race([testsPromise, timeoutPromise]);
|
|
183
189
|
}
|
|
184
190
|
finally {
|
|
185
191
|
if (timerId)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../../src/host/collector.ts"],"names":[],"mappings":"AA8CA,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAIjF"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
const TEST_FILE_SUFFIX = ".test.ts";
|
|
4
|
+
const EXCLUDED_DIR_NAMES = new Set(["node_modules", ".git"]);
|
|
5
|
+
async function walkDirectory(dirPath) {
|
|
6
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
7
|
+
const found = await Promise.all(entries.map((entry) => {
|
|
8
|
+
if (entry.isDirectory()) {
|
|
9
|
+
return EXCLUDED_DIR_NAMES.has(entry.name) ? [] : walkDirectory(join(dirPath, entry.name));
|
|
10
|
+
}
|
|
11
|
+
if (entry.isFile() && entry.name.endsWith(TEST_FILE_SUFFIX)) {
|
|
12
|
+
return [join(dirPath, entry.name)];
|
|
13
|
+
}
|
|
14
|
+
return [];
|
|
15
|
+
}));
|
|
16
|
+
return found.flat();
|
|
17
|
+
}
|
|
18
|
+
async function findTestFiles(absolutePath) {
|
|
19
|
+
let fileInfo;
|
|
20
|
+
try {
|
|
21
|
+
fileInfo = await stat(absolutePath);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err.code === "ENOENT") {
|
|
25
|
+
throw new Error(`Path does not exist: ${absolutePath}`, { cause: err });
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Unable to access path "${absolutePath}": ${err.message}`, { cause: err });
|
|
28
|
+
}
|
|
29
|
+
if (fileInfo.isDirectory()) {
|
|
30
|
+
return walkDirectory(absolutePath);
|
|
31
|
+
}
|
|
32
|
+
if (absolutePath.endsWith(TEST_FILE_SUFFIX)) {
|
|
33
|
+
return [absolutePath];
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`Not a *${TEST_FILE_SUFFIX} file: ${absolutePath}`);
|
|
36
|
+
}
|
|
37
|
+
export async function collectTestSuitePaths(srcPaths) {
|
|
38
|
+
const results = await Promise.all(srcPaths.map((p) => findTestFiles(resolve(p))));
|
|
39
|
+
return [...new Set(results.flat())].sort();
|
|
40
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Device } from "frida";
|
|
2
|
+
export type DeviceSelector = "usb" | "local" | "remote" | {
|
|
3
|
+
id: string;
|
|
4
|
+
} | {
|
|
5
|
+
host: string;
|
|
6
|
+
certificate?: string;
|
|
7
|
+
origin?: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
keepaliveInterval?: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function resolveDevice(selector?: DeviceSelector): Promise<Device>;
|
|
12
|
+
//# sourceMappingURL=device.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../../src/host/device.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAGpC,MAAM,MAAM,cAAc,GACtB,KAAK,GACL,OAAO,GACP,QAAQ,GACR;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GACd;IACE,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEN,wBAAsB,aAAa,CAAC,QAAQ,GAAE,cAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAcvF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import frida from "frida";
|
|
2
|
+
export async function resolveDevice(selector = "local") {
|
|
3
|
+
if (selector === "usb")
|
|
4
|
+
return frida.getUsbDevice();
|
|
5
|
+
if (selector === "local")
|
|
6
|
+
return frida.getLocalDevice();
|
|
7
|
+
if (selector === "remote")
|
|
8
|
+
return frida.getRemoteDevice();
|
|
9
|
+
if ("id" in selector)
|
|
10
|
+
return frida.getDevice(selector.id);
|
|
11
|
+
// Pass network parameters to addRemoteDevice
|
|
12
|
+
const manager = frida.getDeviceManager();
|
|
13
|
+
return manager.addRemoteDevice(selector.host, {
|
|
14
|
+
certificate: selector.certificate,
|
|
15
|
+
origin: selector.origin,
|
|
16
|
+
token: selector.token,
|
|
17
|
+
keepaliveInterval: selector.keepaliveInterval,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
export declare const logger: {
|
|
2
2
|
setVerbose: (value: boolean) => void;
|
|
3
3
|
info: (msg: string) => void;
|
|
4
|
-
log: (msg: string) => void;
|
|
5
4
|
warn: (msg: string) => void;
|
|
6
5
|
error: (msg: string) => void;
|
|
7
|
-
success: (msg: string) => void;
|
|
8
6
|
};
|
|
9
7
|
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/host/logger.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM;IACjB,UAAU,UAAU,OAAO;IAG3B,IAAI,QAAQ,MAAM;IAGlB,IAAI,QAAQ,MAAM;IAClB,KAAK,QAAQ,MAAM;CACpB,CAAC"}
|
|
@@ -8,8 +8,6 @@ export const logger = {
|
|
|
8
8
|
if (verbose)
|
|
9
9
|
console.log(`${chalk.blue("[i]")} ${msg}`);
|
|
10
10
|
},
|
|
11
|
-
|
|
12
|
-
warn: (msg) => console.log(`${chalk.yellow("[!]")} ${msg}`),
|
|
11
|
+
warn: (msg) => console.error(`${chalk.yellow("[!]")} ${msg}`),
|
|
13
12
|
error: (msg) => console.error(`${chalk.red("[!]")} ${msg}`),
|
|
14
|
-
success: (msg) => console.log(`${chalk.green("[✓]")} ${msg}`),
|
|
15
13
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/host/protocol.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEzD,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,CAAC;AAS7B,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAIpE;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,eAAe,EAAE,CAAC;CACtC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const AGENT_MESSAGE_TYPES = {
|
|
2
|
+
"agent-ready": true,
|
|
3
|
+
"test-suite-started": true,
|
|
4
|
+
"test-suite-finished": true,
|
|
5
|
+
"run-finished": true,
|
|
6
|
+
};
|
|
7
|
+
export function isAgentMessage(value) {
|
|
8
|
+
if (typeof value !== "object" || value === null)
|
|
9
|
+
return false;
|
|
10
|
+
const { type } = value;
|
|
11
|
+
return typeof type === "string" && Object.hasOwn(AGENT_MESSAGE_TYPES, type);
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console.d.ts","sourceRoot":"","sources":["../../../src/host/reporter/console.ts"],"names":[],"mappings":"AACA,OAAO,EAAmB,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAExF,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAIrD,CAAC;AAuBF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CASjE"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { logger } from "../logger.js";
|
|
2
2
|
import {} from "../protocol.js";
|
|
3
|
-
const STATUS_SYMBOLS = {
|
|
3
|
+
export const STATUS_SYMBOLS = {
|
|
4
4
|
passed: "✅",
|
|
5
5
|
failed: "❌",
|
|
6
6
|
skipped: "➖",
|
|
@@ -23,16 +23,12 @@ function printTestResult(node, depth = 0) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
export function printTestSuiteResult(suite) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
else if (suite.status == "passed") {
|
|
30
|
-
logger.success(`Test suite "${suite.name}" passed:`);
|
|
31
|
-
}
|
|
26
|
+
const symbol = STATUS_SYMBOLS[suite.status] || "?";
|
|
27
|
+
console.log(`${symbol} Test suite "${suite.name}" ${suite.status}:`);
|
|
32
28
|
if (suite.testResult) {
|
|
33
29
|
printTestResult(suite.testResult, 1);
|
|
34
30
|
}
|
|
35
|
-
else {
|
|
36
|
-
logger.warn(`No results for test suite
|
|
31
|
+
else if (suite.status !== "skipped") {
|
|
32
|
+
logger.warn(`No results for test suite "${suite.name}".`);
|
|
37
33
|
}
|
|
38
34
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../../../src/host/reporter/json.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5F"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../../../src/host/reporter/summary.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAejD,wBAAgB,YAAY,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CA+BzD"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import { STATUS_SYMBOLS } from "./console.js";
|
|
2
3
|
function formatDuration(ms) {
|
|
3
4
|
return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(2)}s`;
|
|
4
5
|
}
|
|
@@ -14,16 +15,16 @@ function formatCount(passed, failed, total) {
|
|
|
14
15
|
export function printSummary(runSummary) {
|
|
15
16
|
const { total, passed, failed, durationMs, testSuitesResults } = runSummary;
|
|
16
17
|
const suitePassed = testSuitesResults.filter((s) => s.status === "passed").length;
|
|
17
|
-
const suiteFailed = testSuitesResults.
|
|
18
|
+
const suiteFailed = testSuitesResults.filter((s) => s.status === "failed").length;
|
|
18
19
|
console.log();
|
|
19
20
|
console.log("------------------------------------------------------------------------------");
|
|
20
21
|
console.log();
|
|
21
22
|
console.log(chalk.bold("Test Suites"));
|
|
22
23
|
for (const suite of testSuitesResults) {
|
|
23
|
-
const
|
|
24
|
-
console.log(` ${
|
|
25
|
-
if (
|
|
26
|
-
for (const line of suite.testResult.
|
|
24
|
+
const symbol = STATUS_SYMBOLS[suite.status] || "?";
|
|
25
|
+
console.log(` ${symbol} ${suite.name}`);
|
|
26
|
+
if (suite.status === "failed" && suite.testResult?.error?.message) {
|
|
27
|
+
for (const line of suite.testResult.error.message.split("\n")) {
|
|
27
28
|
console.log(chalk.red(` ${line}`));
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/host/runner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAA4B,MAAM,OAAO,CAAC;AAE9D,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,qBAAa,UAAU;IAQnB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAV1B,OAAO,CAAC,OAAO,CAAC,CAAU;IAC1B,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,UAAU,CAAC,CAAQ;IAE3B,YACmB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,OAAO,EAC/B;IAEJ,IAAI,YAAY,IAAI,SAAS,eAAe,EAAE,CAE7C;IAEK,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAyBhC;IAEK,QAAQ,IAAI,OAAO,CAAC,UAAU,CAAC,CASpC;IAEK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAkB7B;IAED,OAAO,CAAC,SAAS;CA6BlB"}
|
|
@@ -32,12 +32,12 @@ export class TestRunner {
|
|
|
32
32
|
this.fatalError ??= new Error("Script was destroyed unexpectedly");
|
|
33
33
|
});
|
|
34
34
|
await script.load();
|
|
35
|
-
this.session = session;
|
|
36
|
-
this.script = script;
|
|
37
|
-
this.initialized = true;
|
|
38
35
|
if (this.target.wasSpawned) {
|
|
39
36
|
await this.device.resume(this.target.pid);
|
|
40
37
|
}
|
|
38
|
+
this.session = session;
|
|
39
|
+
this.script = script;
|
|
40
|
+
this.initialized = true;
|
|
41
41
|
}
|
|
42
42
|
catch (error) {
|
|
43
43
|
await session.detach().catch(() => { });
|
|
@@ -55,15 +55,25 @@ export class TestRunner {
|
|
|
55
55
|
return summary;
|
|
56
56
|
}
|
|
57
57
|
async dispose() {
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
if (this.script) {
|
|
59
|
+
try {
|
|
60
|
+
await this.script.unload();
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
logger.warn(`Failed to unload script: ${error.message}`);
|
|
64
|
+
}
|
|
60
65
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
if (this.session) {
|
|
67
|
+
try {
|
|
68
|
+
await this.session.detach();
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
logger.warn(`Failed to detach session: ${error.message}`);
|
|
72
|
+
}
|
|
66
73
|
}
|
|
74
|
+
this.script = undefined;
|
|
75
|
+
this.session = undefined;
|
|
76
|
+
this.initialized = false;
|
|
67
77
|
}
|
|
68
78
|
onMessage(message, _data) {
|
|
69
79
|
if (message.type === "error") {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"target.d.ts","sourceRoot":"","sources":["../../src/host/target.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,OAAO,CAAC;AAE7C,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,OAAO,CAAC;CACrB;AACD,MAAM,MAAM,SAAS,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,SAAS,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7H,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEzF"}
|
|
@@ -26,12 +26,17 @@ export async function resolveTarget(device, targetDef) {
|
|
|
26
26
|
throw new Error("Target process name must not be empty");
|
|
27
27
|
const processes = await device.enumerateProcesses();
|
|
28
28
|
const needle = targetName.toLowerCase();
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const tierFilters = [
|
|
30
|
+
(p) => p.name === targetName,
|
|
31
|
+
(p) => p.name.toLowerCase() === needle,
|
|
32
|
+
(p) => p.name.toLowerCase().includes(needle),
|
|
33
33
|
];
|
|
34
|
-
|
|
34
|
+
let matches = [];
|
|
35
|
+
for (const matchesTier of tierFilters) {
|
|
36
|
+
matches = processes.filter(matchesTier);
|
|
37
|
+
if (matches.length > 0)
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
35
40
|
if (matches.length === 0) {
|
|
36
41
|
throw new Error(`No running process matching '${targetName}' was found.`);
|
|
37
42
|
}
|
|
@@ -47,7 +52,7 @@ export async function resolveTarget(device, targetDef) {
|
|
|
47
52
|
throw new Error("Target identifier must not be empty");
|
|
48
53
|
const apps = await device.enumerateApplications();
|
|
49
54
|
const matchedApp = apps.find((a) => a.identifier === identifier);
|
|
50
|
-
if (!matchedApp || !matchedApp.pid
|
|
55
|
+
if (!matchedApp || !matchedApp.pid) {
|
|
51
56
|
throw new Error(`Application '${identifier}' is not currently running.`);
|
|
52
57
|
}
|
|
53
58
|
return { pid: matchedApp.pid, wasSpawned: false };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frida-test",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"homepage": "https://github.com/bernhste/frida-test/blob/main/README.md",
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
"frida-test": "bin/frida-test.js",
|
|
23
23
|
"frida-test-compile": "bin/frida-test-compile.js"
|
|
24
24
|
},
|
|
25
|
-
"types": "./
|
|
25
|
+
"types": "./src/agent-runtime/globals.d.ts",
|
|
26
26
|
"typesVersions": {
|
|
27
27
|
"*": {
|
|
28
28
|
"protocol.js": [
|
|
29
|
-
"./dist/protocol.d.ts"
|
|
29
|
+
"./dist/host/protocol.d.ts"
|
|
30
30
|
]
|
|
31
31
|
}
|
|
32
32
|
},
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"default": "./src/agent-runtime/globals.js"
|
|
37
37
|
},
|
|
38
38
|
"./protocol.js": {
|
|
39
|
-
"types": "./dist/protocol.d.ts",
|
|
40
|
-
"default": "./dist/protocol.js"
|
|
39
|
+
"types": "./dist/host/protocol.d.ts",
|
|
40
|
+
"default": "./dist/host/protocol.js"
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
@@ -49,16 +49,22 @@
|
|
|
49
49
|
"build": "tsc -b",
|
|
50
50
|
"watch": "tsc -b --watch",
|
|
51
51
|
"clean": "tsc -b --clean && rimraf dist",
|
|
52
|
-
"test:
|
|
53
|
-
"
|
|
52
|
+
"test:unit": "jest",
|
|
53
|
+
"test:integration:android": "npm run build && npx frida-test -U -f 'com.google.android.dialer' ./tests/integration/",
|
|
54
|
+
"prepublishOnly": "npm run clean && npm run build && npm run test:unit && npm run test:integration:android"
|
|
54
55
|
},
|
|
55
56
|
"dependencies": {
|
|
56
57
|
"chalk": "^6.0.0",
|
|
57
58
|
"frida-compile": "^19.0.5"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
61
|
+
"@jest/globals": "^30.5.2",
|
|
62
|
+
"@swc/core": "^1.16.2",
|
|
63
|
+
"@swc/jest": "^0.2.39",
|
|
60
64
|
"@types/frida-gum": "^19.10.0",
|
|
61
|
-
"@types/
|
|
65
|
+
"@types/jest": "^30.0.0",
|
|
66
|
+
"@types/node": "^26.6.2",
|
|
67
|
+
"jest": "^30.5.2",
|
|
62
68
|
"rimraf": "^6.1.3",
|
|
63
69
|
"typescript": "^7.0.2"
|
|
64
70
|
},
|