frooky 0.1.1__tar.gz → 0.1.3__tar.gz

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.
Files changed (30) hide show
  1. frooky-0.1.3/.github/workflows/publish.yml +35 -0
  2. frooky-0.1.3/.github/workflows/sync-labels.yml +117 -0
  3. frooky-0.1.3/.gitignore +44 -0
  4. {frooky-0.1.1 → frooky-0.1.3}/PKG-INFO +1 -1
  5. frooky-0.1.3/docs/examples/example.md +189 -0
  6. frooky-0.1.3/docs/examples/hooks.json +12 -0
  7. frooky-0.1.3/docs/examples/hooks2.json +12 -0
  8. frooky-0.1.3/docs/examples/output.json +7 -0
  9. frooky-0.1.3/docs/usage.md +292 -0
  10. frooky-0.1.3/frooky/__init__.py +8 -0
  11. frooky-0.1.3/frooky/_version.py +34 -0
  12. {frooky-0.1.1 → frooky-0.1.3}/frooky/cli.py +7 -0
  13. {frooky-0.1.1 → frooky-0.1.3}/frooky/frida_runner.py +3 -2
  14. {frooky-0.1.1 → frooky-0.1.3}/frooky.egg-info/PKG-INFO +1 -1
  15. {frooky-0.1.1 → frooky-0.1.3}/frooky.egg-info/SOURCES.txt +9 -0
  16. {frooky-0.1.1 → frooky-0.1.3}/pyproject.toml +5 -2
  17. frooky-0.1.1/frooky/__init__.py +0 -4
  18. {frooky-0.1.1 → frooky-0.1.3}/LICENSE +0 -0
  19. {frooky-0.1.1 → frooky-0.1.3}/README.md +0 -0
  20. {frooky-0.1.1 → frooky-0.1.3}/frooky/__main__.py +0 -0
  21. {frooky-0.1.1 → frooky-0.1.3}/frooky/android/android_decoder.js +0 -0
  22. {frooky-0.1.1 → frooky-0.1.3}/frooky/android/base_script.js +0 -0
  23. {frooky-0.1.1 → frooky-0.1.3}/frooky/android/native_decoder.js +0 -0
  24. {frooky-0.1.1 → frooky-0.1.3}/frooky/ios/base_script.js +0 -0
  25. {frooky-0.1.1 → frooky-0.1.3}/frooky/resources.py +0 -0
  26. {frooky-0.1.1 → frooky-0.1.3}/frooky.egg-info/dependency_links.txt +0 -0
  27. {frooky-0.1.1 → frooky-0.1.3}/frooky.egg-info/entry_points.txt +0 -0
  28. {frooky-0.1.1 → frooky-0.1.3}/frooky.egg-info/requires.txt +0 -0
  29. {frooky-0.1.1 → frooky-0.1.3}/frooky.egg-info/top_level.txt +0 -0
  30. {frooky-0.1.1 → frooky-0.1.3}/setup.cfg +0 -0
@@ -0,0 +1,35 @@
1
+ name: publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ with:
14
+ fetch-depth: 0 # Fetch all history and tags for setuptools-scm
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.x"
18
+ - run: python -m pip install --upgrade build
19
+ - run: python -m build
20
+ - uses: actions/upload-artifact@v4
21
+ with:
22
+ name: dist
23
+ path: dist/*
24
+
25
+ publish:
26
+ needs: build
27
+ runs-on: ubuntu-latest
28
+ permissions:
29
+ id-token: write
30
+ steps:
31
+ - uses: actions/download-artifact@v4
32
+ with:
33
+ name: dist
34
+ path: dist
35
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,117 @@
1
+ name: Sync Labels from Issues to PRs
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, edited, reopened, synchronize]
6
+
7
+ jobs:
8
+ sync-labels:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ pull-requests: write
12
+ issues: read
13
+
14
+ steps:
15
+ - name: Sync labels from linked issues
16
+ uses: actions/github-script@v7
17
+ with:
18
+ script: |
19
+ const prNumber = context.payload.pull_request.number;
20
+ const prBody = context.payload.pull_request.body || '';
21
+
22
+ const issueNumbers = new Set();
23
+
24
+ // Pattern 1: Simple format - fixes #123, closes#456
25
+ const simpleRegex = /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s*#(\d+)/gi;
26
+ const simpleMatches = [...prBody.matchAll(simpleRegex)];
27
+ simpleMatches.forEach(match => issueNumbers.add(parseInt(match[1])));
28
+
29
+ // Pattern 2: Owner/repo format - fixes owner/repo#123
30
+ const ownerRepoRegex = /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+([a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+)#(\d+)/gi;
31
+ const ownerRepoMatches = [...prBody.matchAll(ownerRepoRegex)];
32
+ ownerRepoMatches.forEach(match => {
33
+ const repoPath = match[1];
34
+ const issueNum = parseInt(match[2]);
35
+ // Only add if it's the same repository
36
+ if (repoPath === `${context.repo.owner}/${context.repo.repo}`) {
37
+ issueNumbers.add(issueNum);
38
+ } else {
39
+ console.log(`Skipping cross-repository issue: ${repoPath}#${issueNum}`);
40
+ }
41
+ });
42
+
43
+ // Pattern 3: Full URL format - fixes https://github.com/owner/repo#123 or /issues/123
44
+ const urlRegex = /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+https?:\/\/github\.com\/([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)(?:\/issues\/|#)(\d+)/gi;
45
+ const urlMatches = [...prBody.matchAll(urlRegex)];
46
+ urlMatches.forEach(match => {
47
+ const owner = match[1];
48
+ const repo = match[2];
49
+ const issueNum = parseInt(match[3]);
50
+ // Only add if it's the same repository
51
+ if (owner === context.repo.owner && repo === context.repo.repo) {
52
+ issueNumbers.add(issueNum);
53
+ } else {
54
+ console.log(`Skipping cross-repository issue: ${owner}/${repo}#${issueNum}`);
55
+ }
56
+ });
57
+
58
+ const issueNumbersArray = Array.from(issueNumbers);
59
+
60
+ if (issueNumbersArray.length === 0) {
61
+ console.log('No linked issues found in PR description');
62
+ return;
63
+ }
64
+
65
+ console.log(`Found linked issues: ${issueNumbersArray.join(', ')}`);
66
+
67
+ // Collect all labels from linked issues
68
+ const allLabels = new Set();
69
+
70
+ for (const issueNumber of issueNumbersArray) {
71
+ try {
72
+ const { data: issue } = await github.rest.issues.get({
73
+ owner: context.repo.owner,
74
+ repo: context.repo.repo,
75
+ issue_number: issueNumber
76
+ });
77
+
78
+ console.log(`Issue #${issueNumber} has labels: ${issue.labels.map(l => l.name).join(', ')}`);
79
+
80
+ issue.labels.forEach(label => {
81
+ allLabels.add(label.name);
82
+ });
83
+ } catch (error) {
84
+ console.log(`Error fetching issue #${issueNumber}: ${error.message}`);
85
+ }
86
+ }
87
+
88
+ if (allLabels.size === 0) {
89
+ console.log('No labels found on linked issues');
90
+ return;
91
+ }
92
+
93
+ console.log(`Syncing labels to PR: ${Array.from(allLabels).join(', ')}`);
94
+
95
+ // Get current PR labels
96
+ const { data: currentPR } = await github.rest.pulls.get({
97
+ owner: context.repo.owner,
98
+ repo: context.repo.repo,
99
+ pull_number: prNumber
100
+ });
101
+
102
+ const currentLabels = new Set(currentPR.labels.map(l => l.name));
103
+
104
+ // Add new labels from issues
105
+ const labelsToAdd = Array.from(allLabels).filter(label => !currentLabels.has(label));
106
+
107
+ if (labelsToAdd.length > 0) {
108
+ await github.rest.issues.addLabels({
109
+ owner: context.repo.owner,
110
+ repo: context.repo.repo,
111
+ issue_number: prNumber,
112
+ labels: labelsToAdd
113
+ });
114
+ console.log(`Added labels: ${labelsToAdd.join(', ')}`);
115
+ } else {
116
+ console.log('All labels already present on PR');
117
+ }
@@ -0,0 +1,44 @@
1
+ # Frooky local debug artifacts
2
+ tmp/
3
+ output.json
4
+ *.jsonl
5
+
6
+ # node
7
+ node_modules/
8
+ package.json
9
+ package-lock.json
10
+
11
+
12
+ # Python bytecode
13
+ __pycache__/
14
+ *.py[cod]
15
+ *$py.class
16
+
17
+ # Virtual environments
18
+ .venv/
19
+ venv/
20
+ env/
21
+ .env/
22
+
23
+ # Packaging/build outputs
24
+ build/
25
+ dist/
26
+ *.egg-info/
27
+ .eggs/
28
+ pip-wheel-metadata/
29
+
30
+ # Generated version file (created by setuptools-scm during build)
31
+ frooky/_version.py
32
+
33
+ # Test / type-check / lint caches
34
+ .pytest_cache/
35
+ .mypy_cache/
36
+ .ruff_cache/
37
+ .coverage
38
+ coverage.xml
39
+ htmlcov/
40
+
41
+ # IDE / OS
42
+ .DS_Store
43
+ .vscode/
44
+ .idea/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frooky
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Frida-powered hook runner based on JSON hook files.
5
5
  Author-email: Carlos Holguera <holguera.cybersec@gmail.com>, Stefan Bernhardsgrütter <stefan.bernhardsgruetter@redguard.ch>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -0,0 +1,189 @@
1
+ # Example: Hooking EncryptedSharedPreferences Methods
2
+
3
+ In this example we download and install the OWASP MAS [MASTG-DEMO-0060](https://mas.owasp.org/MASTG/demos/android/MASVS-STORAGE/MASTG-DEMO-0060/MASTG-DEMO-0060/) app (App Writing Sensitive Data to Sandbox using EncryptedSharedPreferences), create two hook files to hook methods related to encrypted shared preferences, and run `frooky` with both hook files against the app. We then analyze the output JSON Lines file to see the captured hook events.
4
+
5
+ ## Creating Hook Files
6
+
7
+ First, create a hook file `hooks.json` to hook the `putString` and `putStringSet` methods of `android.app.SharedPreferencesImpl$EditorImpl`:
8
+
9
+ ```json
10
+ {
11
+ "category": "STORAGE",
12
+ "hooks": [
13
+ {
14
+ "class": "android.app.SharedPreferencesImpl$EditorImpl",
15
+ "methods": [
16
+ "putString",
17
+ "putStringSet"
18
+ ]
19
+ }
20
+ ]
21
+ }
22
+ ```
23
+
24
+ To demonstrate merging multiple hook files, we create a second hook file `hooks2.json` to hook the same methods in the underlying implementation class `androidx.security.crypto.EncryptedSharedPreferences$Editor`:
25
+
26
+ ```json
27
+ {
28
+ "category": "STORAGE",
29
+ "hooks": [
30
+ {
31
+ "class": "androidx.security.crypto.EncryptedSharedPreferences$Editor",
32
+ "methods": [
33
+ "putString",
34
+ "putStringSet"
35
+ ]
36
+ }
37
+ ]
38
+ }
39
+ ```
40
+
41
+ ## Running Frooky with Multiple Hook Files
42
+
43
+ ```bash
44
+ frooky -U -n MASTestApp --platform android hooks.json hooks2.json
45
+
46
+ ___ ____ Powered by Frida 17.6.0
47
+ / __\ / _ | _ _ _ _ _ _ Target: MASTestApp
48
+ / _\ | (_) | / _ \ / _ \ | / / | | | |
49
+ / / / / | | | (_) | (_) || < | |_| | Device: Android Emulator 5554 (emulator-5554)
50
+ \/ /_/ |_| \___/ \___/ |_|\_\ \__, | Platform: android
51
+ |___/ Hook files: 2
52
+ Output: output.json
53
+
54
+ Press Ctrl+C to stop...
55
+
56
+
57
+ Resolved Hooks: 4
58
+ Events: 6 | Last: androidx.security.crypto.EncryptedSharedPreferences$Editor.p...
59
+
60
+ Stopping ...
61
+ ```
62
+
63
+ Notice how `frooky` indicates the Frida version, target app, device, platform, and output file.
64
+
65
+ It reports `Hook files: 2` and `Resolved Hooks: 4`, indicating that hooks from both files were merged and set up successfully.
66
+
67
+ The "Events" line shows the total number of captured hook events (6 in this case) and the last hooked method called.
68
+
69
+ ## Analyzing the Output
70
+
71
+ The output file `output.json` will contain the captured hook events:
72
+
73
+ ```json
74
+ {
75
+ "type": "summary",
76
+ "hooks": [
77
+ {
78
+ "class": "android.app.SharedPreferencesImpl$EditorImpl",
79
+ "method": "putString",
80
+ "overloads": [
81
+ {
82
+ "args": [
83
+ "java.lang.String",
84
+ "java.lang.String"
85
+ ]
86
+ }
87
+ ]
88
+ },
89
+ {
90
+ "class": "android.app.SharedPreferencesImpl$EditorImpl",
91
+ "method": "putStringSet",
92
+ "overloads": [
93
+ {
94
+ "args": [
95
+ "java.lang.String",
96
+ "java.util.Set"
97
+ ]
98
+ }
99
+ ]
100
+ },
101
+ {
102
+ "class": "androidx.security.crypto.EncryptedSharedPreferences$Editor",
103
+ "method": "putString",
104
+ "overloads": [
105
+ {
106
+ "args": [
107
+ "java.lang.String",
108
+ "java.lang.String"
109
+ ]
110
+ }
111
+ ]
112
+ },
113
+ {
114
+ "class": "androidx.security.crypto.EncryptedSharedPreferences$Editor",
115
+ "method": "putStringSet",
116
+ "overloads": [
117
+ {
118
+ "args": [
119
+ "java.lang.String",
120
+ "java.util.Set"
121
+ ]
122
+ }
123
+ ]
124
+ }
125
+ ],
126
+ "totalHooks": 4,
127
+ "errors": [],
128
+ "totalErrors": 0
129
+ }
130
+ {
131
+ "id": "98862c65-de96-4872-95fc-c367b90c68a0",
132
+ "type": "hook",
133
+ "category": "STORAGE",
134
+ "time": "2026-01-19T10:45:50.454Z",
135
+ "class": "android.app.SharedPreferencesImpl$EditorImpl",
136
+ "method": "putString",
137
+ "instanceId": 31636504,
138
+ "stackTrace": [
139
+ "android.app.SharedPreferencesImpl$EditorImpl.putString(Native Method)",
140
+ "androidx.security.crypto.EncryptedSharedPreferences$Editor.putEncryptedObject(EncryptedSharedPreferences.java:389)",
141
+ "androidx.security.crypto.EncryptedSharedPreferences$Editor.putString(EncryptedSharedPreferences.java:262)",
142
+ "androidx.security.crypto.EncryptedSharedPreferences$Editor.putString(Native Method)",
143
+ "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:33)",
144
+ "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)",
145
+ "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)",
146
+ "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)"
147
+ ],
148
+ "inputParameters": [
149
+ {
150
+ "declaredType": "java.lang.String",
151
+ "value": "AQMRC7NJHrnwtE5suFMVSkzr7Zz0m55Yz/Gt4MQ8jXR9LQ+W"
152
+ },
153
+ {
154
+ "declaredType": "java.lang.String",
155
+ "value": "AX4R5MZvOLo+hzcBjDtSvkF+ryFEcXM66M/nzU33MpDv6fh//WWbG93gDW6f4JFXsRvq8WHJNI4zIoalUw=="
156
+ }
157
+ ],
158
+ "returnValue": [
159
+ {
160
+ "declaredType": "android.content.SharedPreferences$Editor",
161
+ "value": "<instance: android.content.SharedPreferences$Editor, $className: android.app.SharedPreferencesImpl$EditorImpl>",
162
+ "runtimeType": "android.app.SharedPreferencesImpl$EditorImpl",
163
+ "instanceId": "31636504",
164
+ "instanceToString": "android.app.SharedPreferencesImpl$EditorImpl@1e2bc18"
165
+ }
166
+ ]
167
+ }
168
+ ... <TRUNCATED FOR BREVITY> ...
169
+ ```
170
+
171
+ We can see the `summary` event at the start, followed by individual `hook` events capturing calls to the hooked methods along with their parameters, return values, and stack traces.
172
+
173
+ The summary event indicates no errors and a total of 4 resolved hooks:
174
+
175
+ - `android.app.SharedPreferencesImpl$EditorImpl.putString`
176
+ - `android.app.SharedPreferencesImpl$EditorImpl.putStringSet`
177
+ - `androidx.security.crypto.EncryptedSharedPreferences$Editor.putString`
178
+ - `androidx.security.crypto.EncryptedSharedPreferences$Editor.putStringSet`
179
+
180
+ The first hook event shows a call to `android.app.SharedPreferencesImpl$EditorImpl.putString`, capturing the input parameters (the key and encrypted value) and the return value (the editor instance). The stack trace provides context on where the method was called from within the app, specifically from `MastgTest.mastgTest`.
181
+
182
+ The input parameters in this event are:
183
+
184
+ - Key: `"AQMRC7NJHrnwtE5suFMVSkzr7Zz0m55Yz/Gt4MQ8jXR9LQ+W"`
185
+ - Encrypted Value: `"AX4R5MZvOLo+hzcBjDtSvkF+ryFEcXM66M/nzU33MpDv6fh//WWbG93gDW6f4JFXsRvq8WHJNI4zIoalUw=="`
186
+
187
+ Which represents the data being stored in the encrypted shared preferences. The "key" being the identifier for the stored value, and the "encrypted value" being the actual data stored in an encrypted format.
188
+
189
+ The return value indicates that the method returned an instance of `android.content.SharedPreferences$Editor`, allowing for method chaining.
@@ -0,0 +1,12 @@
1
+ {
2
+ "category": "STORAGE",
3
+ "hooks": [
4
+ {
5
+ "class": "android.app.SharedPreferencesImpl$EditorImpl",
6
+ "methods": [
7
+ "putString",
8
+ "putStringSet"
9
+ ]
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "category": "STORAGE",
3
+ "hooks": [
4
+ {
5
+ "class": "androidx.security.crypto.EncryptedSharedPreferences$Editor",
6
+ "methods": [
7
+ "putString",
8
+ "putStringSet"
9
+ ]
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,7 @@
1
+ {"type": "summary", "hooks": [{"class": "android.app.SharedPreferencesImpl$EditorImpl", "method": "putString", "overloads": [{"args": ["java.lang.String", "java.lang.String"]}]}, {"class": "android.app.SharedPreferencesImpl$EditorImpl", "method": "putStringSet", "overloads": [{"args": ["java.lang.String", "java.util.Set"]}]}, {"class": "androidx.security.crypto.EncryptedSharedPreferences$Editor", "method": "putString", "overloads": [{"args": ["java.lang.String", "java.lang.String"]}]}, {"class": "androidx.security.crypto.EncryptedSharedPreferences$Editor", "method": "putStringSet", "overloads": [{"args": ["java.lang.String", "java.util.Set"]}]}], "totalHooks": 4, "errors": [], "totalErrors": 0}
2
+ {"id": "98862c65-de96-4872-95fc-c367b90c68a0", "type": "hook", "category": "STORAGE", "time": "2026-01-19T10:45:50.454Z", "class": "android.app.SharedPreferencesImpl$EditorImpl", "method": "putString", "instanceId": 31636504, "stackTrace": ["android.app.SharedPreferencesImpl$EditorImpl.putString(Native Method)", "androidx.security.crypto.EncryptedSharedPreferences$Editor.putEncryptedObject(EncryptedSharedPreferences.java:389)", "androidx.security.crypto.EncryptedSharedPreferences$Editor.putString(EncryptedSharedPreferences.java:262)", "androidx.security.crypto.EncryptedSharedPreferences$Editor.putString(Native Method)", "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:33)", "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)"], "inputParameters": [{"declaredType": "java.lang.String", "value": "AQMRC7NJHrnwtE5suFMVSkzr7Zz0m55Yz/Gt4MQ8jXR9LQ+W"}, {"declaredType": "java.lang.String", "value": "AX4R5MZvOLo+hzcBjDtSvkF+ryFEcXM66M/nzU33MpDv6fh//WWbG93gDW6f4JFXsRvq8WHJNI4zIoalUw=="}], "returnValue": [{"declaredType": "android.content.SharedPreferences$Editor", "value": "<instance: android.content.SharedPreferences$Editor, $className: android.app.SharedPreferencesImpl$EditorImpl>", "runtimeType": "android.app.SharedPreferencesImpl$EditorImpl", "instanceId": "31636504", "instanceToString": "android.app.SharedPreferencesImpl$EditorImpl@1e2bc18"}]}
3
+ {"id": "4f9e6e0b-e150-4957-8cdf-1f95b8002aa4", "type": "hook", "category": "STORAGE", "time": "2026-01-19T10:45:50.448Z", "class": "androidx.security.crypto.EncryptedSharedPreferences$Editor", "method": "putString", "instanceId": 183362555, "stackTrace": ["androidx.security.crypto.EncryptedSharedPreferences$Editor.putString(Native Method)", "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:33)", "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", "java.lang.Thread.run(Thread.java:1012)"], "inputParameters": [{"declaredType": "java.lang.String", "value": "EncryptedAWSKey"}, {"declaredType": "java.lang.String", "value": "AKIAABCDEFGHIJKLMNOP"}], "returnValue": [{"declaredType": "android.content.SharedPreferences$Editor", "value": "<instance: android.content.SharedPreferences$Editor, $className: androidx.security.crypto.EncryptedSharedPreferences$Editor>", "runtimeType": "androidx.security.crypto.EncryptedSharedPreferences$Editor", "instanceId": "183362555", "instanceToString": "androidx.security.crypto.EncryptedSharedPreferences$Editor@aede3fb"}]}
4
+ {"id": "de5037ee-e9f6-44c8-b1ad-a08c7fa123b8", "type": "hook", "category": "STORAGE", "time": "2026-01-19T10:45:50.464Z", "class": "android.app.SharedPreferencesImpl$EditorImpl", "method": "putString", "instanceId": 31636504, "stackTrace": ["android.app.SharedPreferencesImpl$EditorImpl.putString(Native Method)", "androidx.security.crypto.EncryptedSharedPreferences$Editor.putEncryptedObject(EncryptedSharedPreferences.java:389)", "androidx.security.crypto.EncryptedSharedPreferences$Editor.putString(EncryptedSharedPreferences.java:262)", "androidx.security.crypto.EncryptedSharedPreferences$Editor.putString(Native Method)", "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:34)", "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)"], "inputParameters": [{"declaredType": "java.lang.String", "value": "AQMRC7OWD6/h1iJseuzJVrClpwKE8swB8gOrGnsdaN4="}, {"declaredType": "java.lang.String", "value": "AX4R5MaQJvV8rAXOUTIQ+VZz4OzP5jiyPLkdlQK2sMOlgrXuuIp+7DRPPr//E3JtSQje/WXRy/JSx+jIwwxIOUTfyCwLOpxbhaknMFeQJGJ1"}], "returnValue": [{"declaredType": "android.content.SharedPreferences$Editor", "value": "<instance: android.content.SharedPreferences$Editor, $className: android.app.SharedPreferencesImpl$EditorImpl>", "runtimeType": "android.app.SharedPreferencesImpl$EditorImpl", "instanceId": "31636504", "instanceToString": "android.app.SharedPreferencesImpl$EditorImpl@1e2bc18"}]}
5
+ {"id": "1987cff2-bf9b-4ba0-ab4d-5ab2b8225fdb", "type": "hook", "category": "STORAGE", "time": "2026-01-19T10:45:50.457Z", "class": "androidx.security.crypto.EncryptedSharedPreferences$Editor", "method": "putString", "instanceId": 183362555, "stackTrace": ["androidx.security.crypto.EncryptedSharedPreferences$Editor.putString(Native Method)", "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:34)", "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", "java.lang.Thread.run(Thread.java:1012)"], "inputParameters": [{"declaredType": "java.lang.String", "value": "GitHubToken"}, {"declaredType": "java.lang.String", "value": "ghp_1234567890abcdefghijklmnopqrstuvABCD"}], "returnValue": [{"declaredType": "android.content.SharedPreferences$Editor", "value": "<instance: android.content.SharedPreferences$Editor, $className: androidx.security.crypto.EncryptedSharedPreferences$Editor>", "runtimeType": "androidx.security.crypto.EncryptedSharedPreferences$Editor", "instanceId": "183362555", "instanceToString": "androidx.security.crypto.EncryptedSharedPreferences$Editor@aede3fb"}]}
6
+ {"id": "ab724a0a-e1fe-43ed-8d5e-0734c2636130", "type": "hook", "category": "STORAGE", "time": "2026-01-19T10:45:50.471Z", "class": "android.app.SharedPreferencesImpl$EditorImpl", "method": "putString", "instanceId": 31636504, "stackTrace": ["android.app.SharedPreferencesImpl$EditorImpl.putString(Native Method)", "androidx.security.crypto.EncryptedSharedPreferences$Editor.putEncryptedObject(EncryptedSharedPreferences.java:389)", "androidx.security.crypto.EncryptedSharedPreferences$Editor.putStringSet(EncryptedSharedPreferences.java:287)", "androidx.security.crypto.EncryptedSharedPreferences$Editor.putStringSet(Native Method)", "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:35)", "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)"], "inputParameters": [{"declaredType": "java.lang.String", "value": "AQMRC7PmmtQeo27DnFbH7Ruc9Gwm36B+0IwxJVaN2xsfZw=="}, {"declaredType": "java.lang.String", "value": "AX4R5MY+JRTS3rMmjIKCV/0sk2z1cS8Bj1McZ75Zzfk1MIh/h+SZjK5L/KMmcnkeFOxpHhVZJp2knj7u8xQH5pSb6xHJOYN+T7ZJf/uvS2VWPEMnUl/op+BxSRq+FjrmH1WhuROAtEdlU8QJFwCNgLD0CdzZlw7l7z4s7MWiiYWN90HWhczjWYBIRRbmoOpIuNF9JkWSKZ2hkixS1ckJShXWnylG+rtvmVs6wMCazCIsl3yr+ABQ2WIx+CxsEZMEXMdK1413B25rO2m2um7b0xOjmGagXj9moesXgIDACXPzjXzOgsGHv93yOmsDuXBqIVHFqjsNDrXkI+VYEwmi44rtkPW9vNw4G3eOMr7DVN3IhVs="}], "returnValue": [{"declaredType": "android.content.SharedPreferences$Editor", "value": "<instance: android.content.SharedPreferences$Editor, $className: android.app.SharedPreferencesImpl$EditorImpl>", "runtimeType": "android.app.SharedPreferencesImpl$EditorImpl", "instanceId": "31636504", "instanceToString": "android.app.SharedPreferencesImpl$EditorImpl@1e2bc18"}]}
7
+ {"id": "27ef8302-d842-4db0-90a8-355d35e163b2", "type": "hook", "category": "STORAGE", "time": "2026-01-19T10:45:50.466Z", "class": "androidx.security.crypto.EncryptedSharedPreferences$Editor", "method": "putStringSet", "instanceId": 183362555, "stackTrace": ["androidx.security.crypto.EncryptedSharedPreferences$Editor.putStringSet(Native Method)", "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:35)", "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", "java.lang.Thread.run(Thread.java:1012)"], "inputParameters": [{"declaredType": "java.lang.String", "value": "preSharedKeys"}, {"declaredType": "java.util.Set", "value": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALfX7kbfFv3pc3JjOHQ=\n-----END PRIVATE KEY-----,-----BEGIN PRIVATE KEY-----\ngJXS9EwpuzK8U1TOgfplwfKEVngCE2D5FNBQWvNmuHHbigmTCabsA=\n-----END PRIVATE KEY-----", "runtimeType": "java.util.HashSet", "instanceId": "110008543", "instanceToString": "[-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALfX7kbfFv3pc3JjOHQ=\n-----END PRIVATE KEY-----, -----BEGIN PRIVATE KEY-----\ngJXS9EwpuzK8U1TOgfplwfKEVngCE2D5FNBQWvNmuHHbigmTCabsA=\n-----END PRIVATE KEY-----]"}], "returnValue": [{"declaredType": "android.content.SharedPreferences$Editor", "value": "<instance: android.content.SharedPreferences$Editor, $className: androidx.security.crypto.EncryptedSharedPreferences$Editor>", "runtimeType": "androidx.security.crypto.EncryptedSharedPreferences$Editor", "instanceId": "183362555", "instanceToString": "androidx.security.crypto.EncryptedSharedPreferences$Editor@aede3fb"}]}
@@ -0,0 +1,292 @@
1
+ # Usage
2
+
3
+ ## Hook Files
4
+
5
+ Hook files use JSON format. When multiple hook files are provided, their `hooks` arrays are merged together.
6
+
7
+ ### Basic Structure
8
+
9
+ ```json
10
+ {
11
+ "category": "STORAGE",
12
+ "hooks": [
13
+ {
14
+ "class": "com.example.MyClass",
15
+ "methods": ["method1", "method2"]
16
+ }
17
+ ]
18
+ }
19
+ ```
20
+
21
+ ### Java/Kotlin Hooks
22
+
23
+ #### Simple Method Hook
24
+
25
+ ```json
26
+ {
27
+ "class": "java.io.File",
28
+ "method": "exists"
29
+ }
30
+ ```
31
+
32
+ #### Multiple Methods
33
+
34
+ ```json
35
+ {
36
+ "class": "java.io.FileOutputStream",
37
+ "methods": ["write", "close", "flush"]
38
+ }
39
+ ```
40
+
41
+ #### Method Overloads
42
+
43
+ Specify exact method signatures using `overloads`:
44
+
45
+ ```json
46
+ {
47
+ "class": "java.io.FileOutputStream",
48
+ "method": "write",
49
+ "overloads": [
50
+ { "args": ["[B"] },
51
+ { "args": ["[B", "int", "int"] },
52
+ { "args": ["int"] }
53
+ ]
54
+ }
55
+ ```
56
+
57
+ #### Stack Traces
58
+
59
+ Control stack trace depth with `maxFrames`:
60
+
61
+ ```json
62
+ {
63
+ "class": "javax.crypto.Cipher",
64
+ "method": "doFinal",
65
+ "maxFrames": 10
66
+ }
67
+ ```
68
+
69
+ ### Native Hooks
70
+
71
+ Native hooks intercept C/C++ functions. Set `native: true` and specify the symbol.
72
+
73
+ #### Basic Native Hook
74
+
75
+ ```json
76
+ {
77
+ "native": true,
78
+ "symbol": "open",
79
+ "module": "libc.so"
80
+ }
81
+ ```
82
+
83
+ #### Argument Descriptors
84
+
85
+ Define how arguments should be captured:
86
+
87
+ ```json
88
+ {
89
+ "native": true,
90
+ "symbol": "write",
91
+ "module": "libc.so",
92
+ "args": [
93
+ { "name": "fd", "type": "int32" },
94
+ { "name": "buf", "type": "bytes", "length": 256 },
95
+ { "name": "count", "type": "int32" }
96
+ ]
97
+ }
98
+ ```
99
+
100
+ #### Dynamic Length from Another Argument
101
+
102
+ Use `lengthInArg` to read length from another argument:
103
+
104
+ ```json
105
+ {
106
+ "native": true,
107
+ "symbol": "send",
108
+ "module": "libc.so",
109
+ "args": [
110
+ { "name": "sockfd", "type": "int32" },
111
+ { "name": "buf", "type": "bytes", "lengthInArg": 2 },
112
+ { "name": "len", "type": "int32" },
113
+ { "name": "flags", "type": "int32" }
114
+ ]
115
+ }
116
+ ```
117
+
118
+ #### Capture Return Values
119
+
120
+ Set `returnValue: true` on the last argument:
121
+
122
+ ```json
123
+ {
124
+ "native": true,
125
+ "symbol": "read",
126
+ "module": "libc.so",
127
+ "args": [
128
+ { "name": "fd", "type": "int32" },
129
+ { "name": "buf", "type": "bytes", "lengthInArg": 2 },
130
+ { "name": "count", "type": "int32" },
131
+ { "name": "result", "type": "int32", "returnValue": true }
132
+ ]
133
+ }
134
+ ```
135
+
136
+ #### Outbound Parameters
137
+
138
+ Use `direction: "out"` for output parameters that should be read after the function returns:
139
+
140
+ ```json
141
+ {
142
+ "native": true,
143
+ "symbol": "CCCrypt",
144
+ "module": "libcommonCrypto.dylib",
145
+ "args": [
146
+ { "name": "op", "type": "int32" },
147
+ { "name": "alg", "type": "int32" },
148
+ { "name": "dataOut", "type": "bytes", "length": 256, "direction": "out" },
149
+ { "name": "dataOutMoved", "type": "pointer", "direction": "out" }
150
+ ]
151
+ }
152
+ ```
153
+
154
+ #### Filter by Value
155
+
156
+ Only capture events when arguments match specific values:
157
+
158
+ ```json
159
+ {
160
+ "native": true,
161
+ "symbol": "open",
162
+ "module": "libc.so",
163
+ "args": [
164
+ { "name": "pathname", "type": "string", "filter": ["/data/", "/sdcard/"] }
165
+ ]
166
+ }
167
+ ```
168
+
169
+ #### Filter by Stack Trace
170
+
171
+ Only capture events when the call stack contains specific patterns:
172
+
173
+ ```json
174
+ {
175
+ "native": true,
176
+ "symbol": "SSL_write",
177
+ "module": "libssl.so",
178
+ "filterEventsByStacktrace": ["com.example.network", "okhttp3"]
179
+ }
180
+ ```
181
+
182
+ #### Debug Mode
183
+
184
+ Enable verbose logging for troubleshooting:
185
+
186
+ ```json
187
+ {
188
+ "native": true,
189
+ "symbol": "problematic_function",
190
+ "module": "libfoo.so",
191
+ "debug": true
192
+ }
193
+ ```
194
+
195
+ ### Argument Types
196
+
197
+ | Type | Description |
198
+ |------|-------------|
199
+ | `string` | Null-terminated C string |
200
+ | `int32` | 32-bit signed integer |
201
+ | `uint32` | 32-bit unsigned integer |
202
+ | `int64` | 64-bit signed integer |
203
+ | `pointer` | Memory address |
204
+ | `bytes` | Raw bytes (requires `length` or `lengthInArg`) |
205
+ | `bool` | Boolean value |
206
+ | `double` | 64-bit floating point |
207
+ | `CFData` | iOS CFData object |
208
+ | `CFDictionary` | iOS CFDictionary object |
209
+
210
+ ### iOS Objective-C Hooks
211
+
212
+ Hook Objective-C methods using `objClass` and `symbol`:
213
+
214
+ ```json
215
+ {
216
+ "native": true,
217
+ "objClass": "NSURLSession",
218
+ "symbol": "dataTaskWithRequest:completionHandler:"
219
+ }
220
+ ```
221
+
222
+ ## Output Format
223
+
224
+ Events are written to the output file in JSON Lines format (one JSON object per line, known as NDJSON). You can easily pretty-print it e.g. using `jq . output.json`.
225
+
226
+ First of all, a summary event is written when hooking is initialized, listing all resolved hooks. It includes:
227
+
228
+ - `type`: Indicates this is a summary event
229
+ - `hooks`: An array of all hooked methods with their classes and overloads
230
+ - `totalHooks`: Total number of hooks that were set up
231
+ - `errors`: Any errors encountered while setting up hooks
232
+ - `totalErrors`: Total number of errors encountered
233
+
234
+ After that, individual hook events are written each time a hooked method/function is called.
235
+
236
+ Example hook event (pretty-printed for clarity):
237
+
238
+ ```json
239
+ {
240
+ "id": "0117229c-b034-4676-ba33-075fc27922ba",
241
+ "type": "hook",
242
+ "category": "STORAGE",
243
+ "time": "2026-01-18T16:17:25.470Z",
244
+ "class": "android.app.SharedPreferencesImpl$EditorImpl",
245
+ "method": "putString",
246
+ "instanceId": 268282727,
247
+ "stackTrace": [
248
+ "android.app.SharedPreferencesImpl$EditorImpl.putString(Native Method)",
249
+ "androidx.security.crypto.EncryptedSharedPreferences$Editor.putEncryptedObject(EncryptedSharedPreferences.java:389)",
250
+ ...
251
+ ],
252
+ "inputParameters": [
253
+ {
254
+ "declaredType": "java.lang.String",
255
+ "value": "AQMRC7OWD6/h1iJseuzJVrClpwKE8swB8gOrGnsdaN4="
256
+ },
257
+ {
258
+ "declaredType": "java.lang.String",
259
+ "value": "AX4R5MZu+J1p0U3hvKyuEnJDQopI+wupiSi8CAG8dzq0PU76NbbebjhqMtqCD7fFUy2SmmQuQVDlDrrj30d3GQes+PlD8HmRFszVTge039GQ"
260
+ }
261
+ ],
262
+ "returnValue": [
263
+ {
264
+ "declaredType": "android.content.SharedPreferences$Editor",
265
+ "value": "<instance: android.content.SharedPreferences$Editor, $className: android.app.SharedPreferencesImpl$EditorImpl>",
266
+ "runtimeType": "android.app.SharedPreferencesImpl$EditorImpl",
267
+ "instanceId": "268282727",
268
+ "instanceToString": "android.app.SharedPreferencesImpl$EditorImpl@ffdab67"
269
+ }
270
+ ]
271
+ }
272
+ ```
273
+
274
+ Explanation of fields:
275
+
276
+ - `id`: Unique identifier for the event (UUID)
277
+ - `type`: Type of event (e.g., "hook", "summary")
278
+ - `category`: Category specified in the hook file (e.g., "STORAGE", "CRYPTO")
279
+ - `time`: Timestamp of the event in ISO 8601 format
280
+ - `class`: Hooked class name
281
+ - `method`: Hooked method name
282
+ - `instanceId`: Unique identifier for the instance on which the method was called
283
+ - `stackTrace`: Captured stack trace leading to the method call
284
+ - `inputParameters`: Array of input parameters with their declared types and values
285
+ - `declaredType`: The declared type of the parameter
286
+ - `value`: The captured value of the parameter
287
+ - `returnValue`: Array of return values with their declared types and values
288
+ - `declaredType`: The declared type of the return value
289
+ - `value`: The captured value of the return value
290
+ - `runtimeType`: The actual runtime type of the return value
291
+ - `instanceId`: Unique identifier for the return value instance
292
+ - `instanceToString`: String representation of the return value instance
@@ -0,0 +1,8 @@
1
+ """Frooky package."""
2
+
3
+ try:
4
+ from ._version import version as __version__
5
+ except ImportError:
6
+ __version__ = "0+unknown" # Using a PEP 440 compliant local version to make it obvious this is not a real release.
7
+
8
+ __all__ = ["__version__"]
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.3'
32
+ __version_tuple__ = version_tuple = (0, 1, 3)
33
+
34
+ __commit_id__ = commit_id = 'gf9dc6e318'
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import argparse
4
4
  from pathlib import Path
5
5
 
6
+ from . import __version__
6
7
  from .frida_runner import FrookyRunner, RunnerOptions
7
8
 
8
9
 
@@ -12,6 +13,12 @@ def build_parser() -> argparse.ArgumentParser:
12
13
  description="Run Frooky hooks using Frida's Python bindings.",
13
14
  )
14
15
 
16
+ parser.add_argument(
17
+ "--version",
18
+ action="version",
19
+ version=f"frooky {__version__}",
20
+ )
21
+
15
22
  # Device selection group
16
23
  device_group = parser.add_argument_group("device selection")
17
24
  device_group.add_argument("-D", "--device", metavar="ID", help="Connect to device with the given ID")
@@ -46,6 +46,7 @@ class FrookyRunner:
46
46
  self.session: Optional[frida.core.Session] = None
47
47
  self.script: Optional[frida.core.Script] = None
48
48
  self.device: Optional[frida.core.Device] = None
49
+ self.spawned_pid: Optional[int] = None
49
50
  self.event_count: int = 0
50
51
  self.last_event: str = "Waiting for events..."
51
52
  self.total_hooks: Optional[int] = None
@@ -325,6 +326,7 @@ class FrookyRunner:
325
326
  elif opts.spawn:
326
327
  pid = self.device.spawn(opts.spawn)
327
328
  session = self.device.attach(pid)
329
+ self.spawned_pid = pid
328
330
  return session
329
331
 
330
332
  else:
@@ -387,8 +389,7 @@ class FrookyRunner:
387
389
 
388
390
  # Resume if spawned
389
391
  if self.options.spawn:
390
- pid = self.session.pid
391
- self.device.resume(pid)
392
+ self.device.resume(self.spawned_pid)
392
393
 
393
394
  # Main loop
394
395
  while True:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frooky
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Frida-powered hook runner based on JSON hook files.
5
5
  Author-email: Carlos Holguera <holguera.cybersec@gmail.com>, Stefan Bernhardsgrütter <stefan.bernhardsgruetter@redguard.ch>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -1,8 +1,17 @@
1
+ .gitignore
1
2
  LICENSE
2
3
  README.md
3
4
  pyproject.toml
5
+ .github/workflows/publish.yml
6
+ .github/workflows/sync-labels.yml
7
+ docs/usage.md
8
+ docs/examples/example.md
9
+ docs/examples/hooks.json
10
+ docs/examples/hooks2.json
11
+ docs/examples/output.json
4
12
  frooky/__init__.py
5
13
  frooky/__main__.py
14
+ frooky/_version.py
6
15
  frooky/cli.py
7
16
  frooky/frida_runner.py
8
17
  frooky/resources.py
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["setuptools>=70"]
2
+ requires = ["setuptools>=70", "setuptools-scm>=8"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "frooky"
7
- version = "0.1.1"
7
+ dynamic = ["version"]
8
8
  description = "Frida-powered hook runner based on JSON hook files."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -50,3 +50,6 @@ frooky = [
50
50
  "android/*.js",
51
51
  "ios/*.js",
52
52
  ]
53
+
54
+ [tool.setuptools_scm]
55
+ version_file = "frooky/_version.py"
@@ -1,4 +0,0 @@
1
- """Frooky package."""
2
-
3
- __all__ = ["__version__"]
4
- __version__ = "0.1.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes