344 lines
12 KiB
Bash
Executable File
344 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# reverse-skill case initializer (bash parity of case-init.ps1 + offline/ctf presets)
|
|
# Usage:
|
|
# bash skills/scripts/case-init.sh --hint "apk reverse" --case-name demo
|
|
# bash skills/scripts/case-init.sh --hint "local sample" --preset offline-sample --sample ./app.apk
|
|
# bash skills/scripts/case-init.sh --hint "ctf web" --preset ctf-public --target-url https://chal.example
|
|
set -euo pipefail
|
|
|
|
HINT=""
|
|
CASE_NAME=""
|
|
PACKAGE_ROOT=""
|
|
AUTH_STATUS=""
|
|
AUTH_GRANTED=0
|
|
AUTH_BASIS="unknown"
|
|
EVIDENCE_OF_AUTH=""
|
|
TARGET_URL=""
|
|
NETWORK_PROFILE=""
|
|
SAMPLE=""
|
|
PRESET=""
|
|
IN_SCOPE_ASSETS=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-Hint|--hint) HINT="${2:-}"; shift 2 ;;
|
|
-CaseName|--case-name) CASE_NAME="${2:-}"; shift 2 ;;
|
|
-PackageRoot|--package-root) PACKAGE_ROOT="${2:-}"; shift 2 ;;
|
|
-AuthStatus|--auth-status) AUTH_STATUS="${2:-}"; shift 2 ;;
|
|
-AuthGranted|--auth-granted) AUTH_GRANTED=1; shift ;;
|
|
-AuthBasis|--auth-basis) AUTH_BASIS="${2:-}"; shift 2 ;;
|
|
-EvidenceOfAuth|--evidence-of-auth) EVIDENCE_OF_AUTH="${2:-}"; shift 2 ;;
|
|
-TargetUrl|--target-url) TARGET_URL="${2:-}"; shift 2 ;;
|
|
-NetworkProfile|--network-profile) NETWORK_PROFILE="${2:-}"; shift 2 ;;
|
|
-Sample|--sample|--offline-sample) SAMPLE="${2:-}"; shift 2 ;;
|
|
-Preset|--preset) PRESET="${2:-}"; shift 2 ;;
|
|
-InScopeAssets|--in-scope-asset) IN_SCOPE_ASSETS+=("${2:-}"); shift 2 ;;
|
|
-h|--help) sed -n '2,8p' "$0"; exit 0 ;;
|
|
*)
|
|
if [[ -z "$HINT" && "$1" != -* ]]; then HINT="$1"; shift
|
|
else echo "Unknown arg: $1" >&2; exit 2; fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SKILLS_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
if [[ -z "$PACKAGE_ROOT" ]]; then
|
|
PACKAGE_ROOT="$(cd "$SKILLS_ROOT/.." && pwd)"
|
|
fi
|
|
|
|
# Presets: reduce "AI refuses to work" friction for legitimate local/CTF work.
|
|
case "$PRESET" in
|
|
"" ) ;;
|
|
offline-sample|own-sample|local-sample)
|
|
AUTH_GRANTED=1
|
|
AUTH_STATUS="granted"
|
|
AUTH_BASIS="own_system"
|
|
NETWORK_PROFILE="${NETWORK_PROFILE:-offline}"
|
|
EVIDENCE_OF_AUTH="${EVIDENCE_OF_AUTH:-preset:offline-sample (owner-operated local file)}"
|
|
if [[ -n "$SAMPLE" ]]; then IN_SCOPE_ASSETS+=("$SAMPLE"); fi
|
|
;;
|
|
ctf-public|ctf)
|
|
AUTH_GRANTED=1
|
|
AUTH_STATUS="granted"
|
|
AUTH_BASIS="ctf_public"
|
|
NETWORK_PROFILE="${NETWORK_PROFILE:-authorized_target_only}"
|
|
EVIDENCE_OF_AUTH="${EVIDENCE_OF_AUTH:-preset:ctf-public}"
|
|
;;
|
|
own-system|lab-only)
|
|
AUTH_GRANTED=1
|
|
AUTH_STATUS="granted"
|
|
AUTH_BASIS="own_system"
|
|
NETWORK_PROFILE="${NETWORK_PROFILE:-lab_only}"
|
|
EVIDENCE_OF_AUTH="${EVIDENCE_OF_AUTH:-preset:own-system/lab}"
|
|
;;
|
|
*)
|
|
echo "WARN: unknown preset '$PRESET' (allowed: offline-sample|ctf-public|own-system)" >&2
|
|
;;
|
|
esac
|
|
|
|
if [[ -z "$CASE_NAME" ]]; then
|
|
slug="$(printf '%s' "$HINT" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/^$/case/' | cut -c1-40)"
|
|
CASE_NAME="$(date +%Y%m%d-%H%M%S)-${slug}"
|
|
fi
|
|
|
|
# CaseName is a directory name, never a path. Match the PowerShell guard while
|
|
# keeping localized letters available under UTF-8 locales.
|
|
case_name_length="$(printf '%s' "$CASE_NAME" | wc -m | tr -d '[:space:]')"
|
|
if [[ -z "$CASE_NAME" || "$case_name_length" -gt 80 ||
|
|
! "$CASE_NAME" =~ ^[[:alnum:]] ||
|
|
"$CASE_NAME" =~ [/:\\*?\"\<\>\|] ||
|
|
"$CASE_NAME" =~ [[:cntrl:]] ||
|
|
"$CASE_NAME" =~ [\.[:space:]]$ ]]; then
|
|
echo "Invalid --case-name '$CASE_NAME'. Use a 1-80 character directory name beginning with a letter or number; path separators, trailing dots/spaces, control characters, and wildcard characters are not allowed." >&2
|
|
exit 2
|
|
fi
|
|
if [[ -n "$NETWORK_PROFILE" ]]; then
|
|
network_profile_normalized="$(printf '%s' "$NETWORK_PROFILE" | tr '[:upper:]' '[:lower:]')"
|
|
case "$network_profile_normalized" in
|
|
offline|lab_only|authorized_target_only|unrestricted_lab|lab|authorized|auth|offline_only) ;;
|
|
*)
|
|
echo "Invalid --network-profile '$NETWORK_PROFILE'. Allowed: offline, lab_only, authorized_target_only, unrestricted_lab (aliases: lab, authorized, auth, offline_only)." >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
CASE_ROOT="$PACKAGE_ROOT/work/$CASE_NAME"
|
|
mkdir -p "$CASE_ROOT/evidence" "$CASE_ROOT/notes" "$CASE_ROOT/report"
|
|
|
|
auth_status_resolved="pending"
|
|
if [[ $AUTH_GRANTED -eq 1 ]]; then auth_status_resolved="granted"; fi
|
|
if [[ -n "$AUTH_STATUS" ]]; then
|
|
candidate="$(printf '%s' "$AUTH_STATUS" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
|
|
case "$candidate" in
|
|
pending|granted|denied|unknown) auth_status_resolved="$candidate" ;;
|
|
*) echo "WARN: ignoring invalid --auth-status '$AUTH_STATUS'" >&2 ;;
|
|
esac
|
|
fi
|
|
|
|
if [[ -n "$EVIDENCE_OF_AUTH" ]]; then
|
|
evidence_auth="$EVIDENCE_OF_AUTH"
|
|
elif [[ $AUTH_GRANTED -eq 1 || "$auth_status_resolved" == "granted" ]]; then
|
|
evidence_auth="cli-flag AuthGranted or AuthStatus=granted"
|
|
else
|
|
evidence_auth="FILL_ME"
|
|
fi
|
|
|
|
ASSETS=()
|
|
[[ -n "$TARGET_URL" ]] && ASSETS+=("$TARGET_URL")
|
|
[[ -n "$SAMPLE" ]] && ASSETS+=("$SAMPLE")
|
|
for a in "${IN_SCOPE_ASSETS[@]:-}"; do
|
|
[[ -n "$a" ]] || continue
|
|
dup=0
|
|
for e in "${ASSETS[@]:-}"; do [[ "$e" == "$a" ]] && dup=1 && break; done
|
|
[[ $dup -eq 0 ]] && ASSETS+=("$a")
|
|
done
|
|
if [[ ${#ASSETS[@]} -eq 0 && "$HINT" =~ https?://([^[:space:]/]+) ]]; then
|
|
ASSETS+=("https://${BASH_REMATCH[1]}/")
|
|
fi
|
|
|
|
if [[ -n "$NETWORK_PROFILE" ]]; then
|
|
network_mode="$NETWORK_PROFILE"
|
|
elif [[ "$auth_status_resolved" == "granted" && ${#ASSETS[@]} -gt 0 && -z "$SAMPLE" ]]; then
|
|
# Authorized network targets follow the PowerShell default. Offline is
|
|
# reserved for an explicit local sample, never for a URL by accident.
|
|
network_mode="authorized_target_only"
|
|
else
|
|
network_mode="offline"
|
|
fi
|
|
network_mode="$(printf '%s' "$network_mode" | tr '[:upper:]' '[:lower:]')"
|
|
case "$network_mode" in
|
|
lab) network_mode="lab_only" ;;
|
|
authorized|auth) network_mode="authorized_target_only" ;;
|
|
offline_only) network_mode="offline" ;;
|
|
offline|lab_only|authorized_target_only|unrestricted_lab) ;;
|
|
*)
|
|
echo "Invalid --network-profile '$network_mode'. Allowed: offline, lab_only, authorized_target_only, unrestricted_lab (aliases: lab, authorized, auth, offline_only)." >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
# Route PRIMARY via bash master-route
|
|
ROUTE_TMP="$(mktemp -d "${TMPDIR:-/tmp}/rs-case-route.XXXXXX")"
|
|
primary="reverse-engineering/SKILL.md"
|
|
primary_id="R0"
|
|
if [[ -f "$SCRIPT_DIR/master-route.sh" ]]; then
|
|
set +e
|
|
bash "$SCRIPT_DIR/master-route.sh" --hint "$HINT" --out-dir "$ROUTE_TMP" >/dev/null 2>&1
|
|
set -e
|
|
if [[ -f "$ROUTE_TMP/route-scope.md" ]]; then
|
|
rt="$(cat "$ROUTE_TMP/route-scope.md")"
|
|
if printf '%s\n' "$rt" | grep -Eq '^[[:space:]]*-[[:space:]]*primary_skill:[[:space:]]*skills/'; then
|
|
primary="$(printf '%s\n' "$rt" | grep -E '^[[:space:]]*-[[:space:]]*primary_skill:[[:space:]]*skills/' | head -1 | sed -E 's/.*skills\///' | tr -d '\r')"
|
|
fi
|
|
if printf '%s\n' "$rt" | grep -Eq '^[[:space:]]*-[[:space:]]*primary:[[:space:]]*'; then
|
|
primary_id="$(printf '%s\n' "$rt" | grep -E '^[[:space:]]*-[[:space:]]*primary:[[:space:]]*' | head -1 | sed -E 's/.*primary:[[:space:]]*//' | tr -d '\r')"
|
|
fi
|
|
fi
|
|
fi
|
|
rm -rf "$ROUTE_TMP"
|
|
|
|
created="$(date -Iseconds 2>/dev/null || date +%Y-%m-%dT%H:%M:%S%z)"
|
|
if [[ ${#ASSETS[@]} -gt 0 ]]; then
|
|
assets_block="$(printf ' - %s\n' "${ASSETS[@]}")"
|
|
else
|
|
assets_block=" []"
|
|
fi
|
|
|
|
ready=false
|
|
if [[ "$auth_status_resolved" == "granted" ]]; then
|
|
if [[ "$network_mode" == "offline" ]]; then
|
|
if [[ -n "$SAMPLE" && ${#ASSETS[@]} -gt 0 ]]; then ready=true; fi
|
|
elif [[ ${#ASSETS[@]} -gt 0 ]]; then
|
|
ready=true
|
|
fi
|
|
fi
|
|
|
|
check_auth="[ ]"; [[ "$auth_status_resolved" == "granted" ]] && check_auth="[x]"
|
|
check_scope="[ ]"; [[ ${#ASSETS[@]} -gt 0 || "$network_mode" == "offline" ]] && check_scope="[x]"
|
|
check_net="[ ]"; [[ -n "$network_mode" ]] && check_net="[x]"
|
|
ready_str="false"; [[ "$ready" == true ]] && ready_str="true"
|
|
|
|
if [[ "$ready" == true ]]; then
|
|
timeline_next="open PRIMARY SKILL.md and ACT within scope"
|
|
timeline_summary="case directory created; scope ready_for_act=true"
|
|
else
|
|
timeline_next="fill scope auth + in_scope; set ready_for_act"
|
|
timeline_summary="case directory created; scope pending auth"
|
|
fi
|
|
|
|
cat > "$CASE_ROOT/scope.md" <<EOF
|
|
# Case Scope
|
|
|
|
## meta
|
|
- case_id: $CASE_NAME
|
|
- created: $created
|
|
- operator: local
|
|
- primary_skill: $primary
|
|
- primary_id: $primary_id
|
|
- lead_role: lead
|
|
- specialist_roles: []
|
|
- hint: $HINT
|
|
- preset: ${PRESET:-none}
|
|
|
|
## auth
|
|
- status: $auth_status_resolved
|
|
- basis: $AUTH_BASIS
|
|
- evidence_of_auth: $evidence_auth
|
|
- MUST NOT proceed if status != granted
|
|
|
|
## in_scope
|
|
- assets:
|
|
$assets_block
|
|
- surfaces: []
|
|
- activities: []
|
|
|
|
## out_of_scope
|
|
- assets: []
|
|
- activities: [dos, phishing_real_users, unrestricted_exfil]
|
|
|
|
## network_profile
|
|
- mode: $network_mode
|
|
- notes: |
|
|
offline | lab_only | authorized_target_only | unrestricted_lab
|
|
Change mode only after auth.status = granted.
|
|
Presets: offline-sample | ctf-public | own-system
|
|
|
|
## deliverables
|
|
- report: true
|
|
- field_journal: true
|
|
- diagrams: true
|
|
- timeline: true
|
|
|
|
## constraints
|
|
- timebox: {}
|
|
- stealth: low
|
|
- data_handling: anonymize
|
|
|
|
## signoff
|
|
- ready_for_act: $ready_str
|
|
- checklist:
|
|
- $check_auth auth.status = granted
|
|
- $check_scope in_scope.assets non-empty OR offline sample path set
|
|
- $check_net network_profile.mode chosen
|
|
- [ ] out_of_scope reviewed
|
|
- [ ] roles assigned (see skills/ops/role-map.md)
|
|
|
|
## ops_refs
|
|
- skills/ops/scope-contract.md
|
|
- skills/ops/evidence-finding-path.md
|
|
- skills/ops/role-map.md
|
|
- skills/ops/timeline-workitem.md
|
|
- skills/ops/IDENTITY.md
|
|
EOF
|
|
|
|
cat > "$CASE_ROOT/timeline.md" <<EOF
|
|
# Timeline (append-only)
|
|
|
|
## $created | lead | init
|
|
- action: case-init
|
|
- command_or_ref: skills/scripts/case-init.sh
|
|
- result_summary: $timeline_summary
|
|
- artifacts: [scope.md, workitems.md]
|
|
- evidence_ids: []
|
|
- next: $timeline_next
|
|
EOF
|
|
|
|
cat > "$CASE_ROOT/workitems.md" <<'EOF'
|
|
# Work Items
|
|
|
|
| ID | title | role | targets | surface | status | evidence | notes |
|
|
|----|-------|------|---------|---------|--------|----------|-------|
|
|
| WI-001 | Establish scope and auth | lead | case | process | in_progress | | |
|
|
|
|
## Coverage
|
|
- [ ] Recon/analysis complete for in_scope assets
|
|
- [ ] Critical/High candidates triaged (or N/A for pure RE)
|
|
- [ ] Validated findings have Evidence (E-*)
|
|
- [ ] Path documented (attack/call/solve)
|
|
- [ ] Timeline continuous across major phases
|
|
- [ ] Report via docs-generator
|
|
- [ ] field-journal anonymized
|
|
|
|
## Refs
|
|
- skills/ops/timeline-workitem.md
|
|
- skills/ops/evidence-finding-path.md
|
|
EOF
|
|
|
|
if [[ "$ready" == true ]]; then
|
|
cat > "$CASE_ROOT/README.md" <<EOF
|
|
# Case $CASE_NAME
|
|
|
|
1. Scope is ready_for_act=true (auth granted + in_scope set)
|
|
2. Open primary skill: skills/$primary
|
|
3. Append \`timeline.md\`; update \`workitems.md\`
|
|
4. Append Evidence under \`evidence/\`
|
|
5. Promote findings with Evidence chain (skills/ops/evidence-finding-path.md)
|
|
6. Report via docs-generator; journal via field-journal
|
|
EOF
|
|
else
|
|
cat > "$CASE_ROOT/README.md" <<EOF
|
|
# Case $CASE_NAME
|
|
|
|
1. Edit \`scope.md\` — set auth.status=granted and in_scope
|
|
- or re-run: \`bash skills/scripts/case-init.sh --hint "..." --preset offline-sample --sample ./sample.apk\`
|
|
- or: \`--preset ctf-public --target-url https://...\`
|
|
2. Set ready_for_act when checklist complete
|
|
3. Open primary skill: skills/$primary
|
|
4. Append \`timeline.md\`; update \`workitems.md\`
|
|
5. Promote findings with Evidence chain (skills/ops/evidence-finding-path.md)
|
|
6. Report via docs-generator; journal via field-journal
|
|
EOF
|
|
fi
|
|
|
|
echo "CASE -> $CASE_ROOT"
|
|
echo "PRIMARY skill: skills/$primary ($primary_id)"
|
|
echo "auth.status=$auth_status_resolved network_profile=$network_mode ready_for_act=$ready_str"
|
|
if [[ "$ready" == true ]]; then
|
|
echo "NEXT: open PRIMARY SKILL.md and ACT within scope"
|
|
else
|
|
echo "NEXT: fill scope.md auth + in_scope; then open PRIMARY SKILL.md"
|
|
echo "HINT: try --preset offline-sample|--preset ctf-public|--preset own-system"
|
|
fi
|