fix: make case setup consistent across Windows, Linux, and macOS Owner integration: preserve routing.json SSoT, keep reports optional by default, and require offline samples to resolve to real local files.
364 lines
12 KiB
Bash
Executable File
364 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=""
|
|
PROJECT_ROOT=""
|
|
PACKAGE_ROOT_BOUND=0
|
|
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:-}"; PACKAGE_ROOT_BOUND=1; shift 2 ;;
|
|
-ProjectRoot|--project-root) PROJECT_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
|
|
if [[ -z "$PROJECT_ROOT" ]]; then
|
|
if [[ $PACKAGE_ROOT_BOUND -eq 1 ]]; then
|
|
PROJECT_ROOT="$PACKAGE_ROOT"
|
|
else
|
|
PROJECT_ROOT="$(pwd -P)"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$SAMPLE" ]]; then
|
|
if [[ ! -f "$SAMPLE" ]]; then
|
|
echo "Local --sample file not found: $SAMPLE" >&2
|
|
exit 2
|
|
fi
|
|
sample_dir="$(cd "$(dirname "$SAMPLE")" && pwd -P)"
|
|
SAMPLE="$sample_dir/$(basename "$SAMPLE")"
|
|
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="$PROJECT_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" --project-root "$PROJECT_ROOT" --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
|
|
- project_root: $PROJECT_ROOT
|
|
- 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
|