diff --git a/skills/scripts/case-init.ps1 b/skills/scripts/case-init.ps1 index a7695c3..b53a54b 100644 --- a/skills/scripts/case-init.ps1 +++ b/skills/scripts/case-init.ps1 @@ -4,6 +4,9 @@ # Ready-to-act example: # powershell -File skills/scripts/case-init.ps1 -Hint "web pentest" -CaseName my-case ` # -AuthGranted -TargetUrl "https://app.example/" -NetworkProfile authorized_target_only +# Offline sample ready-to-act example: +# powershell -File skills/scripts/case-init.ps1 -Hint "offline apk" -CaseName my-sample ` +# -Preset offline-sample -Sample ".\app.apk" param( [string] $Hint = '', [string] $CaseName = '', @@ -15,6 +18,8 @@ param( [string] $AuthBasis = 'own_system', [string] $EvidenceOfAuth = '', [string] $TargetUrl = '', + [string] $Sample = '', + [string] $Preset = '', [string[]] $InScopeAssets = @(), [string] $NetworkProfile = '', [switch] $ReadyForAct @@ -35,6 +40,32 @@ $requestedProjectRoot = if (-not [string]::IsNullOrWhiteSpace($ProjectRoot)) { } $projectRoot = Resolve-ReverseProjectRoot -RequestedRoot $requestedProjectRoot +# Cross-platform case presets. Keep semantics aligned with case-init.sh. +$presetNormalized = $Preset.Trim().ToLowerInvariant() +if ($presetNormalized -in @('offline-sample', 'own-sample', 'local-sample')) { + $AuthGranted = $true + $AuthStatus = 'granted' + $AuthBasis = 'own_system' + if ([string]::IsNullOrWhiteSpace($NetworkProfile)) { $NetworkProfile = 'offline' } + if ([string]::IsNullOrWhiteSpace($EvidenceOfAuth)) { + $EvidenceOfAuth = 'preset:offline-sample (owner-operated local file)' + } +} elseif ($presetNormalized -in @('ctf-public', 'ctf')) { + $AuthGranted = $true + $AuthStatus = 'granted' + $AuthBasis = 'ctf_public' + if ([string]::IsNullOrWhiteSpace($NetworkProfile)) { $NetworkProfile = 'authorized_target_only' } + if ([string]::IsNullOrWhiteSpace($EvidenceOfAuth)) { $EvidenceOfAuth = 'preset:ctf-public' } +} elseif ($presetNormalized -in @('own-system', 'lab-only')) { + $AuthGranted = $true + $AuthStatus = 'granted' + $AuthBasis = 'own_system' + if ([string]::IsNullOrWhiteSpace($NetworkProfile)) { $NetworkProfile = 'lab_only' } + if ([string]::IsNullOrWhiteSpace($EvidenceOfAuth)) { $EvidenceOfAuth = 'preset:own-system/lab' } +} elseif (-not [string]::IsNullOrWhiteSpace($Preset)) { + Write-Host ("WARN: unknown -Preset '{0}' (allowed: offline-sample|ctf-public|own-system)" -f $Preset) -ForegroundColor Yellow +} + if (-not $CaseName) { $slug = if ($Hint) { ($Hint.ToLowerInvariant() -replace '[^a-z0-9]+', '-').Trim('-') @@ -90,6 +121,9 @@ $evidenceAuth = if (-not [string]::IsNullOrWhiteSpace($EvidenceOfAuth)) { $assets = New-Object System.Collections.Generic.List[string] if (-not [string]::IsNullOrWhiteSpace($TargetUrl)) { [void]$assets.Add($TargetUrl.Trim()) } +if (-not [string]::IsNullOrWhiteSpace($Sample) -and -not $assets.Contains($Sample.Trim())) { + [void]$assets.Add($Sample.Trim()) +} foreach ($a in @($InScopeAssets)) { if (-not [string]::IsNullOrWhiteSpace($a) -and -not $assets.Contains($a.Trim())) { [void]$assets.Add($a.Trim()) @@ -103,8 +137,8 @@ if ($assets.Count -eq 0 -and $Hint -match 'https?://([^\s/]+)') { $networkMode = 'offline' if (-not [string]::IsNullOrWhiteSpace($NetworkProfile)) { $networkMode = $NetworkProfile.Trim() -} elseif ($assets.Count -gt 0 -and $authStatusResolved -eq 'granted') { - # training labs / intentional vulns often use lab_only; default authorized_target_only +} elseif ($assets.Count -gt 0 -and $authStatusResolved -eq 'granted' -and [string]::IsNullOrWhiteSpace($Sample)) { + # Authorized network targets default to target-only. Explicit local samples remain offline. $networkMode = 'authorized_target_only' } # normalize common aliases @@ -122,19 +156,21 @@ if ($networkMode -notin $allowedNetworkModes) { throw "Invalid -NetworkProfile '$NetworkProfile'. Allowed: offline, lab_only, authorized_target_only, unrestricted_lab (aliases: lab, authorized, auth, offline_only)." } -# ready_for_act requires auth granted + assets + non-offline network. -# -ReadyForAct cannot skip auth (would bypass hard gate). +# ready_for_act requires auth granted + assets. Network targets need a non-offline +# profile; an explicit local sample is valid in offline mode. -ReadyForAct never +# bypasses auth or scope. $ready = $false $netAllowsAct = ($networkMode -ne 'offline' -and -not [string]::IsNullOrWhiteSpace($networkMode)) -if ($authStatusResolved -eq 'granted' -and $assets.Count -gt 0 -and $netAllowsAct) { +$offlineSampleReady = ($networkMode -eq 'offline' -and -not [string]::IsNullOrWhiteSpace($Sample) -and $assets.Count -gt 0) +if ($authStatusResolved -eq 'granted' -and $assets.Count -gt 0 -and ($netAllowsAct -or $offlineSampleReady)) { $ready = $true } elseif ($ReadyForAct) { if ($authStatusResolved -ne 'granted') { Write-Host 'WARN: -ReadyForAct ignored because auth.status is not granted' -ForegroundColor Yellow } elseif ($assets.Count -eq 0) { Write-Host 'WARN: -ReadyForAct ignored because in_scope.assets is empty' -ForegroundColor Yellow - } elseif (-not $netAllowsAct) { - Write-Host 'WARN: -ReadyForAct ignored because network_profile is offline/empty' -ForegroundColor Yellow + } elseif (-not $netAllowsAct -and -not $offlineSampleReady) { + Write-Host 'WARN: -ReadyForAct ignored because offline mode requires an explicit -Sample' -ForegroundColor Yellow } } @@ -193,6 +229,7 @@ $scope = @" - lead_role: lead - specialist_roles: [] - hint: $Hint +- preset: $(if ([string]::IsNullOrWhiteSpace($Preset)) { 'none' } else { $Preset }) ## auth - status: $authStatusResolved @@ -293,7 +330,9 @@ $readmeNext = if ($ready) { "@ } else { @" -1. Edit ``scope.md`` — set auth.status=granted and in_scope (or re-run with -AuthGranted -TargetUrl) +1. Edit ``scope.md`` — set auth.status=granted and in_scope + - network target: re-run with ``-AuthGranted -TargetUrl `` + - local sample: re-run with ``-Preset offline-sample -Sample `` 2. Set ready_for_act when checklist complete 3. Open primary skill: skills/$primary 4. Append ``timeline.md``; update ``workitems.md``