mirror of
https://github.com/trailofbits/skills.git
synced 2026-09-14 14:28:48 +08:00
Fix firebase-apk-scanner scanner.sh path and command handoff (#269)
* Fix firebase-apk-scanner scanner.sh path and command handoff
scanner.sh sat at the plugin root, but SKILL.md invoked it as
{baseDir}/scanner.sh, where {baseDir} is the skill directory, so the skill's
central action pointed at a nonexistent path. Move the script into
skills/firebase-apk-scanner/ with git mv so the path resolves; the SKILL.md
references are unchanged.
The firebase-apk-scanner skill sets disable-model-invocation: true, so the
model cannot invoke it and the command body's "Invoke the skill" instruction
was a dead end. Rework commands/scan-apk.md to read and follow the skill's
SKILL.md workflow directly, resolving the skill base directory via the plugin
root environment variable.
Fix README usage: the command is /trailofbits:scan-apk (not /firebase-scan),
and the standalone script path is now skills/firebase-apk-scanner/scanner.sh.
Bump version 2.1.1 -> 2.2.0.
Claude-Session: https://claude.ai/code/session_014UFqJu1ada7gXjD9peo1rX
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* firebase-apk-scanner: make the command reachable and stop reporting empty scans as clean
Claude Code ignores `name` in a command file, so `name: trailofbits:scan-apk`
never took effect and `/trailofbits:scan-apk` — the only entry point the README
documented — was unreachable. Commands namespace by plugin directory name, as
insecure-defaults already does with no `name` key, so drop the key and document
`/firebase-apk-scanner:scan-apk`.
A directory holding no .apk files ran the loop zero times and still printed
"Vulnerable: 0" in green at exit 0, and an APK that failed to decompile counted
toward "Total APKs" while counting toward nothing else, so "1 scanned, 0
vulnerable" covered an APK that was never tested. Both now fail: an empty
directory exits 1, an all-failed run exits 1, and a partial failure reports
"Failed to scan: N" and carries failed_apks in scan_report.json. The skill's
summary table gained the matching row. Also match *.APK, which was silently
skipped and fed the zero-item pass.
The summary block passed colors as printf arguments while the color variables
held a literal \033, so those lines printed the escape as four characters. Use
ANSI-C quoting so the variables carry real ESC bytes in both positions.
Give the command a plugin-root fallback and an abort, matching c-review, so it
does not dead-end under Codex where CLAUDE_PLUGIN_ROOT is unset, and say that
$ARGUMENTS in the workflow file is literal text to substitute rather than a
shell variable to expand.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UFqJu1ada7gXjD9peo1rX
* firebase-apk-scanner: stop calling an untested APK secure
extract_firebase_config always writes a config file, emitting [] for every field
it found nothing for, so the CONFIG_FAILED branch only fires if the write itself
fails. The case that actually happens — the APK decompiles but carries no
Firebase config, because it is obfuscated or does not use Firebase — skipped
every endpoint test on its own guard, left apk_vulnerable false, and wrote
SECURE. Output was "Total APKs: 1 / Vulnerable: 0" in green at exit 0 with zero
endpoints probed, byte-identical to a real clean scan.
Detect the all-empty config, mark the APK NO_CONFIG, count it in UNTESTED_APKS,
and report it on its own line and as untested_apks in scan_report.json. A run
where every APK either failed or had no config now exits 1, since nothing was
tested. The skill's summary table gained the row and says what NO_CONFIG means.
Verified with a stub apktool that decompiles to an empty tree: before, exit 0
and status SECURE; after, exit 1, status NO_CONFIG, untested_apks 1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UFqJu1ada7gXjD9peo1rX
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: kz-tob <kara.zaffarano@trailofbits.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "firebase-apk-scanner",
|
||||
"version": "2.1.1",
|
||||
"version": "2.2.0",
|
||||
"description": "Scan Android APKs for Firebase security misconfigurations including open databases, storage buckets, authentication issues, and exposed cloud functions. For authorized security research only.",
|
||||
"author": {
|
||||
"name": "Nick Sellier",
|
||||
|
||||
@@ -62,15 +62,15 @@ sudo apt install apktool curl jq unzip binutils
|
||||
## Usage
|
||||
|
||||
```
|
||||
/firebase-scan ./app.apk
|
||||
/firebase-scan ./apks/
|
||||
/firebase-apk-scanner:scan-apk ./app.apk
|
||||
/firebase-apk-scanner:scan-apk ./apks/
|
||||
```
|
||||
|
||||
Or run the standalone script directly:
|
||||
Or run the standalone script directly, from the plugin directory:
|
||||
|
||||
```bash
|
||||
./scanner.sh app.apk
|
||||
./scanner.sh ./apks/ --no-cleanup
|
||||
./skills/firebase-apk-scanner/scanner.sh app.apk
|
||||
./skills/firebase-apk-scanner/scanner.sh ./apks/ --no-cleanup
|
||||
```
|
||||
|
||||
## Vulnerability Categories
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
name: trailofbits:scan-apk
|
||||
description: Scans Android APKs for Firebase security misconfigurations
|
||||
argument-hint: "<apk-file-or-directory>"
|
||||
allowed-tools: Bash Read Grep Glob
|
||||
@@ -9,6 +8,32 @@ allowed-tools: Bash Read Grep Glob
|
||||
|
||||
**Arguments:** $ARGUMENTS
|
||||
|
||||
Parse the APK path from arguments. If empty, ask for the path.
|
||||
Parse the APK file or directory from the arguments. If empty, ask the user for the path.
|
||||
|
||||
Invoke the `firebase-apk-scanner` skill with the APK path for the full workflow.
|
||||
This command is the entry point for a Firebase APK scan. The `firebase-apk-scanner` skill
|
||||
sets `disable-model-invocation: true`, so it cannot be invoked as a skill from here. Read
|
||||
its workflow file and follow it directly.
|
||||
|
||||
Resolve the plugin root, then confirm the skill's workflow and scanner are present. The
|
||||
`ls` echoes the expanded absolute paths, which is how you learn the value to use below:
|
||||
|
||||
```bash
|
||||
ls "${CLAUDE_PLUGIN_ROOT}/skills/firebase-apk-scanner/SKILL.md" \
|
||||
"${CLAUDE_PLUGIN_ROOT}/skills/firebase-apk-scanner/scanner.sh"
|
||||
```
|
||||
|
||||
If that fails (under Codex `CLAUDE_PLUGIN_ROOT` is unset, so the paths collapse to
|
||||
`/skills/...`), search for the plugin instead:
|
||||
|
||||
```bash
|
||||
find ~/.claude ~/.codex . -path '*/plugins/firebase-apk-scanner/skills/firebase-apk-scanner/scanner.sh' -print -quit 2>/dev/null
|
||||
```
|
||||
|
||||
Strip the trailing `/skills/firebase-apk-scanner/scanner.sh` to get the root. If neither
|
||||
resolves, **stop** and report the paths searched — do not continue with an empty root.
|
||||
|
||||
Then read `<root>/skills/firebase-apk-scanner/SKILL.md` and carry out its workflow against
|
||||
the parsed path. Within that workflow, `{baseDir}` is `<root>/skills/firebase-apk-scanner`,
|
||||
so `{baseDir}/scanner.sh` is the scanner confirmed above. `$ARGUMENTS` appears throughout
|
||||
that file as literal text, not a shell variable — substitute the path you parsed above
|
||||
wherever it occurs, and never run a command with a bare `$ARGUMENTS` still in it.
|
||||
|
||||
@@ -96,8 +96,17 @@ Present findings in this format:
|
||||
|--------|-------|
|
||||
| APKs Scanned | X |
|
||||
| Vulnerable | X |
|
||||
| Failed to scan | X |
|
||||
| No Firebase config | X |
|
||||
| Total Issues | X |
|
||||
|
||||
Take these from `failed_apks` and `untested_apks` in `scan_report.json`. Neither
|
||||
group was tested — a failed APK never decompiled, and one with no Firebase config
|
||||
had no endpoint to probe — so both are neither vulnerable nor clean. Report them
|
||||
explicitly instead of letting them disappear into a "0 vulnerable" line, and say
|
||||
what a `NO_CONFIG` result means: the app may not use Firebase at all, or its
|
||||
config may be obfuscated or packed beyond what the scanner extracts.
|
||||
|
||||
## Extracted Configuration
|
||||
|
||||
| Field | Value |
|
||||
|
||||
+50
-9
@@ -6,16 +6,18 @@
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
# Colors for output. ANSI-C quoting so these hold real ESC bytes: they are passed as
|
||||
# printf arguments as well as embedded in format strings, and a literal \033 in an
|
||||
# argument prints as the four characters rather than setting the color.
|
||||
RED=$'\033[0;31m'
|
||||
GREEN=$'\033[0;32m'
|
||||
YELLOW=$'\033[1;33m'
|
||||
BLUE=$'\033[0;34m'
|
||||
# CYAN intentionally unused but kept for consistency with other color definitions
|
||||
# shellcheck disable=SC2034
|
||||
CYAN='\033[0;36m'
|
||||
MAGENTA='\033[0;35m'
|
||||
NC='\033[0m'
|
||||
CYAN=$'\033[0;36m'
|
||||
MAGENTA=$'\033[0;35m'
|
||||
NC=$'\033[0m'
|
||||
|
||||
# Configuration
|
||||
TIMEOUT_SECONDS=10
|
||||
@@ -34,6 +36,8 @@ JSON_REPORT="${OUTPUT_DIR}/scan_report.json"
|
||||
TOTAL_APKS=0
|
||||
VULNERABLE_APKS=0
|
||||
TOTAL_VULNS=0
|
||||
FAILED_APKS=0
|
||||
UNTESTED_APKS=0
|
||||
|
||||
# Common Cloud Function names to enumerate
|
||||
COMMON_FUNCTIONS="addMessage sendMessage createUser deleteUser updateUser getUser getUsers login logout register signup signUp authenticate verify verifyEmail resetPassword changePassword sendNotification sendEmail processPayment createOrder getOrders updateOrder deleteOrder uploadFile getFile generateToken validateToken refreshToken getData setData syncData backup restore export import webhook callback api admin debug test healthcheck status createProfile updateProfile deleteProfile getProfile subscribe unsubscribe notify push analytics"
|
||||
@@ -1008,6 +1012,7 @@ process_apk() {
|
||||
if ! apktool d -f -o "$apk_decompiled" "$apk_path" >/dev/null 2>&1; then
|
||||
log_error "Failed to decompile: $apk_path"
|
||||
echo "DECOMPILE_FAILED" >"${apk_result_dir}/status.txt"
|
||||
FAILED_APKS=$((FAILED_APKS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -1021,6 +1026,7 @@ process_apk() {
|
||||
if [ ! -f "$config_file" ]; then
|
||||
log_error "Failed to create config file"
|
||||
echo "CONFIG_FAILED" >"${apk_result_dir}/status.txt"
|
||||
FAILED_APKS=$((FAILED_APKS + 1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -1053,6 +1059,17 @@ process_apk() {
|
||||
log_info " API Keys: $key_count"
|
||||
log_info " Function Names: $func_count"
|
||||
|
||||
# Every endpoint test below is guarded on one of these values, so with none of them
|
||||
# there is nothing to probe. Reporting that as SECURE is indistinguishable from an
|
||||
# APK whose endpoints were tested and found locked down.
|
||||
if [ "$((db_count + proj_count + bucket_count + key_count + func_count))" -eq 0 ]; then
|
||||
log_warning "No Firebase configuration found in $apk_name — nothing was tested"
|
||||
log_warning "The app may not use Firebase, or the config may be obfuscated or packed."
|
||||
echo "NO_CONFIG" >"${apk_result_dir}/status.txt"
|
||||
UNTESTED_APKS=$((UNTESTED_APKS + 1))
|
||||
return 0
|
||||
fi
|
||||
|
||||
local apk_vulnerable=false
|
||||
local apk_vulns=""
|
||||
local anonymous_token=""
|
||||
@@ -1274,6 +1291,8 @@ generate_report() {
|
||||
echo " \"scanner_version\": \"1.0\","
|
||||
echo " \"total_apks\": $TOTAL_APKS,"
|
||||
echo " \"vulnerable_apks\": $VULNERABLE_APKS,"
|
||||
echo " \"failed_apks\": $FAILED_APKS,"
|
||||
echo " \"untested_apks\": $UNTESTED_APKS,"
|
||||
echo " \"total_vulnerabilities\": $TOTAL_VULNS,"
|
||||
echo ' "results": ['
|
||||
|
||||
@@ -1368,10 +1387,16 @@ main() {
|
||||
|
||||
if [ -d "$target" ]; then
|
||||
log_info "Scanning directory: $target"
|
||||
for apk in "$target"/*.apk; do
|
||||
for apk in "$target"/*.apk "$target"/*.APK; do
|
||||
[ -f "$apk" ] || continue
|
||||
process_apk "$apk"
|
||||
done
|
||||
|
||||
if [ "$TOTAL_APKS" -eq 0 ]; then
|
||||
log_error "No .apk files found in: $target"
|
||||
log_error "A scan that inspected nothing is not a clean scan."
|
||||
exit 1
|
||||
fi
|
||||
elif [ -f "$target" ]; then
|
||||
log_info "Scanning single APK: $target"
|
||||
process_apk "$target"
|
||||
@@ -1401,8 +1426,24 @@ main() {
|
||||
printf 'Vulnerable: %s0%s\n' "$GREEN" "$NC"
|
||||
fi
|
||||
|
||||
if [ "$FAILED_APKS" -gt 0 ]; then
|
||||
printf 'Failed to scan: %s%d%s (not tested, so "0 vulnerable" says nothing about these)\n' \
|
||||
"$YELLOW" "$FAILED_APKS" "$NC"
|
||||
fi
|
||||
|
||||
if [ "$UNTESTED_APKS" -gt 0 ]; then
|
||||
printf 'No Firebase config: %s%d%s (nothing to probe, so these were not tested)\n' \
|
||||
"$YELLOW" "$UNTESTED_APKS" "$NC"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Results saved to: $OUTPUT_DIR"
|
||||
|
||||
# An APK that never got tested must not be reported as a clean result.
|
||||
if [ "$((FAILED_APKS + UNTESTED_APKS))" -eq "$TOTAL_APKS" ]; then
|
||||
log_error "No APK was actually tested (all failed to process or carried no Firebase config)."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user