Build a PHP2Shell Triage Kit

Build a PowerShell kit to triage PHP assets and create incident handoffs.

Introduction

30 Second Summary

During a security incident, a long list of affected systems can become overwhelming fast. Responders need a clear way to decide what deserves attention first.

In this project, you will build a defensive PHP2Shell incident triage kit using PowerShell. It will turn fictional PHP asset data into a prioritized response handoff.

What You'll Build

Picture running one script to see the most urgent fictional PHP asset rise to the top of a console queue, with matching CSV and Markdown reports ready for a security operations handoff.

By the end of this project, you'll have:

  • A branch-aware detector that classifies fictional PHP assets as Vulnerable, Patched, Unsupported, or Unknown.
  • A prioritized incident handoff that puts vulnerable internet-facing Apache mod_php assets at the top of matching console, CSV, and Markdown queues.
  • A public GitHub repository that lets reviewers reproduce the workflow from fictional evidence.
  • Secret Mission: Record a SHA256 hash of the source inventory and prove that a harmless evidence change produces a different value.

Are there any prerequisites?

No prior PowerShell or incident-response experience is required. You only need the Windows device you are already using because the guide includes setup for every required tool.

Before We Start

This opening checkpoint connects your defensive PHP2Shell triage kit to the blue-team judgment it demonstrates. It also clarifies why trustworthy fictional asset evidence matters in an incident-response role.

Set Up the Windows Toolkit

Your triage evidence needs a known runtime so another responder can reproduce the result. It also needs a secured home where reviewers can inspect your work.

PowerShell 7.6.7 gives the detector a known runtime. Visual Studio Code 1.143.0 gives you an editor for PowerShell scripts.

The stable Microsoft PowerShell extension adds PowerShell support to the editor. GitHub Free gives your final evidence a secured publishing destination.

In this step, get ready to:
  • Install PowerShell 7.6.7 on Windows.
  • Install Visual Studio Code 1.143.0 with the Microsoft PowerShell extension 2025.4.0.
  • Secure a verified GitHub Free account with two-factor authentication.
Install PowerShell

A fixed PowerShell version keeps the detector consistent between your machine and a reviewer's machine. Start with a version check so you can follow the path that matches your Windows device.

  • Press the Windows key to open Windows search.
  • Type Windows PowerShell into the search box.
  • Press Enter to open Windows PowerShell.
  • Check for the required PowerShell runtime by running this command:
pwsh -Version

What does this command do?

The command asks the installed pwsh executable to print its version. The result tells you whether PowerShell is ready or needs installation.

Choose the tab that matches the result in your terminal.

✔️ I see the required version

Your terminal shows PowerShell 7.6.7. That runtime is ready for the triage script.

ⓧ I see an older version

An older runtime can produce different behavior from the reviewed project environment. Use the official WinGet installation method to install the required release.

  • Install the current PowerShell release through WinGet by running this command:
winget install --id Microsoft.PowerShell --source winget

What does this command do?

WinGet retrieves PowerShell from the official package source. The package installer adds the current PowerShell release to Windows.

ⓧ Command not found

Windows cannot find the pwsh executable yet. Install PowerShell through the official WinGet package.

  • Install PowerShell through WinGet by running this command:
winget install --id Microsoft.PowerShell --source winget

What does this command do?

WinGet downloads the official PowerShell package. Windows then registers the pwsh executable for new terminal sessions.

WinGet unavailable?

The release page contains several assets, so finding the Windows installer can be fiddly. Use the exact x64 MSI file for this project.

Help me troubleshoot my PowerShell installation. You can also compare your steps with the official Windows installation guide.

  • Open the official PowerShell release page.
  • Download PowerShell-7.6.7-win-x64.msi from the release assets.
  • Run the downloaded MSI installer.
  • Complete the installer with its default options.
  • Close the Windows PowerShell window.
  • Press the Windows key to open Windows search.
  • Type PowerShell into the search box.
  • Press Enter to open a fresh terminal session.
  • Verify the installed PowerShell release by running this command:
pwsh -Version

Why run the check again?

A fresh terminal session reloads the commands available through Windows. This check confirms that pwsh now resolves to the required runtime.

You should see PowerShell 7.6.7 in the output. Good progress. Your detector now has the exact runtime used throughout this project.

Still seeing the wrong version?

Close every terminal window after installation. A terminal that stayed open can retain the previous command path.

PowerShell still shows the wrong version.

Install Visual Studio Code and PowerShell support

Visual Studio Code provides the editor where you will build the detector. The Microsoft PowerShell extension adds script debugging plus PowerShell session selection.

  • Check whether the required Visual Studio Code release is already available by running this command in the PowerShell window from earlier:
code --version

What does this command do?

The command prints the installed editor version. It also prints the commit identifier plus the Windows architecture.

Choose the tab that matches the first line of the output.

✔️ I see the required version

The first output line shows 1.143.0. Your editor is ready for the PowerShell extension.

ⓧ I see an older version

The project uses Visual Studio Code 1.143.0 so the editor behavior matches the walkthrough.

ⓧ Command not found

The Visual Studio Code command is unavailable because the editor still needs installation. The Windows User setup adds Visual Studio Code to the Windows command path.

  • Close the PowerShell window after the Visual Studio Code installation finishes.
  • Press the Windows key to open Windows search.
  • Type PowerShell into the search box.
  • Press Enter to open a fresh terminal session.
  • Verify the Visual Studio Code release by running this command:
code --version

Why use a fresh terminal?

The Visual Studio Code installer updates the Windows command path. A fresh PowerShell session loads that updated path before running the version check.

The first line should show 1.143.0. The next lines contain build details for the installed editor.

Visual Studio Code command unavailable?

Confirm that you used the Windows User setup. Close every PowerShell window before checking again.

Help me fix the Visual Studio Code command.

The editor now needs the stable PowerShell extension. Its version matters because later preview releases are outside this project's tested setup.

  • Press the Windows key to open Windows search.
  • Type Visual Studio Code into the search box.
  • Press Enter to open Visual Studio Code.
  • Press Ctrl+Shift+X to open Extensions.
  • Enter PowerShell into the Extensions search field.
  • Select the Microsoft PowerShell extension.

Choose the tab that matches the extension page.

✔️ I see the stable version

The extension page shows version 2025.4.0 as installed. Your editor has the stable PowerShell support required for this project.

ⓧ I see another version

The extension needs the stable 2025.4.0 release used by this walkthrough.

  • Open the control beside Uninstall.
  • Select Install Another Version.
  • Select version 2025.4.0 from the version list.
  • Allow Visual Studio Code to reload if prompted.

ⓧ The extension is not installed

The stable Marketplace release is 2025.4.0 for this project.

  • Click Install on the Microsoft PowerShell extension page.
  • Allow Visual Studio Code to reload if prompted.
  • Confirm that the extension page shows version 2025.4.0 after installation.

Extension installation failing?

Confirm that the extension publisher is Microsoft. Confirm that your internet connection can reach the Visual Studio Marketplace.

Help me install the stable PowerShell extension.

Visual Studio Code can detect several PowerShell installations. Selecting the project runtime ensures that debugging uses PowerShell 7.6.7.

  • Press Ctrl+Shift+P to open the Visual Studio Code Command Palette.
  • Enter PowerShell: Show Session Menu into the Command Palette.
  • Select the installed PowerShell entry that shows version 7.6.7.
  • Return to the Microsoft PowerShell extension page.
  • Confirm that version 2025.4.0 remains installed.

Visual Studio Code now has the stable extension plus the intended PowerShell session. You can use its debugger for the detector in the next step.

Secure GitHub and verify the toolkit

Your final repository will present the triage workflow for review. A verified account with two-factor authentication protects that public evidence from unauthorized account access.

This part touches account security settings. Keep authentication details private while you complete GitHub's official account checklist.

  • Open the official GitHub account setup guide in your browser.
  • Create a GitHub Free account if you do not already have one.
  • Verify the account through the message sent to your email address.
  • Enable two-factor authentication through the account security guidance.
  • Open the GitHub dashboard while signed in.

Before the final check, which runtime, editor, extension, plus account signals do you expect to see?

  • Return to the PowerShell window from earlier.
  • Verify the PowerShell runtime by running this command:
pwsh -Version

What does this check prove?

This check proves that Windows can start the required PowerShell runtime from a normal terminal session. The output should identify version 7.6.7.

You should see PowerShell 7.6.7 in the terminal output.

  • Verify the Visual Studio Code installation by running this command:
code --version

What does this check prove?

This check proves that the Visual Studio Code command is available through Windows. The first output line should identify version 1.143.0.

You should see 1.143.0 on the first line of the output.

  • Return to the Microsoft PowerShell extension page in Visual Studio Code.
  • Confirm that the installed extension version is 2025.4.0.
  • Confirm that the active PowerShell session uses version 7.6.7.
  • Return to the signed-in GitHub dashboard.

You should now have PowerShell 7.6.7, Visual Studio Code 1.143.0, plus the Microsoft PowerShell extension 2025.4.0 ready on Windows. Your verified GitHub account should be signed in with two-factor authentication enabled.

That is the setup finished. Your Windows toolkit now has a reproducible runtime, a PowerShell-aware editor, plus a secured publishing account.

Your toolkit is ready without creating any project files yet. Next, you will build a first-pass detector that produces a useful result before exposing a realistic version-triage mistake.

Build a Naive Detector

Your Windows toolkit is ready for the first triage pass. Security triage often starts by comparing installed versions with a known fixed release.

In Visual Studio Code, you'll build a PowerShell detector around one global threshold. Its first run lets you test whether that shortcut understands separately maintained PHP branches.

In this step, get ready to:
  • Create the local workspace with its fictional asset inventory.
  • Build a PowerShell detector that uses one global fixed-version threshold.
  • Analyze the detector's classification of billing-api.
Create the workspace and fictional inventory

A CSV inventory gives the detector one row per asset. Every record in this project is fictional, so the workflow stays focused on defensive analysis.

  • Return to Visual Studio Code from earlier.
  • Select File in the top menu.
  • Select Open Folder....
  • Select Desktop in the folder dialog.

The folder dialog now points to your Desktop. This keeps the project in a predictable location.

  • Select New Folder.

A name field appears for the new folder.

  • Enter php2shell-triage-kit as the folder name.
  • Press Enter.

The new php2shell-triage-kit folder is now selected.

  • Select Select Folder.

Good, your project now has a home. The Explorer view should show php2shell-triage-kit as the workspace folder.

Visual Studio Code may display a Workspace Trust dialog because this folder is new. You created the folder yourself, so enabling its features is safe for this project.

  • Select Yes, I trust the authors if the Workspace Trust dialog appears.
  • Select php2shell-triage-kit in the Explorer view.
  • Select the New Folder icon.
  • Enter data.
  • Press Enter.

You'll see the new data folder beneath php2shell-triage-kit in the Explorer view.

  • Select php2shell-triage-kit in the Explorer view.
  • Select the New Folder icon.
  • Enter output.
  • Press Enter.

The Explorer view should now show separate data and output folders.

  • Select the data folder.
  • Select the New File... button.
  • Enter php-inventory.csv.
  • Press Enter.

The empty data\php-inventory.csv file opens in the editor.

  • Fill data\php-inventory.csv with the fictional inventory by pasting the following data:
AssetName,Environment,PHPVersion,SAPI,InternetFacing,Criticality
customer-portal,Production,8.5.11,PHP-FPM,Yes,High
billing-api,Production,8.4.27,Apache mod_php,Yes,Critical
intranet-hr,Production,8.3.35,PHP-FPM,No,High
legacy-reports,Production,8.1.33,PHP-CGI,Yes,Medium
dev-sandbox,Development,8.2.35,LiteSpeed,No,Low
partner-gateway,Production,8.4.26,Apache mod_php,Yes,High

What Is This Inventory Modeling?

  • Each row represents one fictional PHP asset.
  • The PHPVersion column gives the detector the value it compares with the fixed release.
  • The remaining columns preserve deployment context for later triage work.
  • The six records include several PHP branches so the first comparison has a realistic test set.
  • Save data\php-inventory.csv by pressing Ctrl+S.
  • Confirm the editor shows one header row followed by six fictional asset records.

Inventory File Not Looking Right?

Confirm that php-inventory.csv sits inside the data folder. Check that every asset remains on its own line.

Make sure the filename ends with .csv.

Help me check my fictional PHP inventory file.

Build the global-threshold detector

The first detector treats 8.5.12 as one project-wide fixed release. It converts every inventory value into a comparable version before assigning a status.

  • Select php2shell-triage-kit in the Explorer view.
  • Select the New File... button.
  • Enter Invoke-PHP2ShellTriage.ps1.
  • Press Enter.

The empty Invoke-PHP2ShellTriage.ps1 file opens beside your inventory.

  • Build the first-pass detector by pasting the following code into Invoke-PHP2ShellTriage.ps1:
[CmdletBinding()]
param(
    [string]$InventoryPath = '.\data\php-inventory.csv'
)

$ErrorActionPreference = 'Stop'
$fixedVersion = [version]'8.5.12'

if (-not (Test-Path -Path $InventoryPath -PathType Leaf)) {
    throw "Inventory file not found: $InventoryPath"
}

$assets = Import-Csv -Path $InventoryPath -Encoding utf8

$results = foreach ($asset in $assets) {
    $version = [version]$asset.PHPVersion
    $status = if ($version -lt $fixedVersion) { 'Vulnerable' } else { 'Patched' }

    [pscustomobject]@{
        AssetName = $asset.AssetName
        PHPVersion = $asset.PHPVersion
        Status = $status
        TargetVersion = "$fixedVersion"
    }
}

$results | Format-Table -AutoSize

What Does This Detector Do?

  • The InventoryPath parameter points to the fictional CSV file.
  • The fixedVersion variable stores the single 8.5.12 target as a [version] value.
  • The file check stops the script with a clear message when the inventory cannot be found.
  • The foreach loop assigns each asset a Vulnerable or Patched status.
  • The final pipeline displays the results as a console table.
  • Save Invoke-PHP2ShellTriage.ps1 by pressing Ctrl+S.
Run the designed false positive

The script now applies the -lt comparison to every imported version. Before you run it, consider whether every asset should be judged against the same 8.5.12 target.

  • Select the Invoke-PHP2ShellTriage.ps1 editor tab.
  • Start the current PowerShell file by pressing F5.

You'll see six assets in the console table. The table includes the AssetName, PHPVersion, Status and TargetVersion columns.

Your first detector is running. That result gives you something concrete to challenge.

You'll see billing-api at 8.4.27 classified as Vulnerable. Its TargetVersion column shows 8.5.12.

That classification is the designed false positive.

Why Does the Global Threshold Fail?

The detector compares PHP 8.4.27 with PHP 8.5.12. The first value is numerically lower, so the script assigns Vulnerable.

PHP maintains separate release branches. The fixed release for PHP 8.4 is 8.4.27, so billing-api is already patched within its branch.

The global comparison ignores that branch boundary. That makes a fast-looking result unreliable for incident scope.

Script Not Producing the Table?

Confirm that Invoke-PHP2ShellTriage.ps1 is the active editor tab before pressing F5.

If the script reports that the inventory cannot be found, confirm that php-inventory.csv is inside the workspace's data folder.

Help me debug my naive PHP triage detector.

✔️ Awesome, I've got everything!

Great, your saved detector runs against all six fictional assets. You have also captured why the billing-api result cannot be trusted.

ⓧ I'd like to double check the full code

Your complete data\php-inventory.csv should look like this:

AssetName,Environment,PHPVersion,SAPI,InternetFacing,Criticality
customer-portal,Production,8.5.11,PHP-FPM,Yes,High
billing-api,Production,8.4.27,Apache mod_php,Yes,Critical
intranet-hr,Production,8.3.35,PHP-FPM,No,High
legacy-reports,Production,8.1.33,PHP-CGI,Yes,Medium
dev-sandbox,Development,8.2.35,LiteSpeed,No,Low
partner-gateway,Production,8.4.26,Apache mod_php,Yes,High

Your complete Invoke-PHP2ShellTriage.ps1 should look like this:

[CmdletBinding()]
param(
    [string]$InventoryPath = '.\data\php-inventory.csv'
)

$ErrorActionPreference = 'Stop'
$fixedVersion = [version]'8.5.12'

if (-not (Test-Path -Path $InventoryPath -PathType Leaf)) {
    throw "Inventory file not found: $InventoryPath"
}

$assets = Import-Csv -Path $InventoryPath -Encoding utf8

$results = foreach ($asset in $assets) {
    $version = [version]$asset.PHPVersion
    $status = if ($version -lt $fixedVersion) { 'Vulnerable' } else { 'Patched' }

    [pscustomobject]@{
        AssetName = $asset.AssetName
        PHPVersion = $asset.PHPVersion
        Status = $status
        TargetVersion = "$fixedVersion"
    }
}

$results | Format-Table -AutoSize

Your detector now produces a visible triage result. Next, you'll replace the global threshold with branch-aware logic that classifies each maintained PHP release line correctly.

Make Version Triage Branch-Aware

Your first PowerShell detector produced a visible triage table. Its global threshold also misclassified a patched asset because it ignored separately maintained release branches.

PHP release branches receive fixes independently. A branch-aware version comparison lets each asset use the correct fixed release.

In this step, get ready to:
  • Map each supported PHP branch to its minimum fixed release.
  • Classify vulnerable, patched, unsupported, and unknown versions.
  • Run the corrected detector and compare its results with the false positive.
Replace the global check with branch-aware logic

The fixed-release map gives every supported branch its own comparison target. Safe defaults also keep malformed values in a manual-review state.

  • Switch back to Invoke-PHP2ShellTriage.ps1 in Visual Studio Code.
  • Locate the line that declares $fixedVersion.
  • Replace that line with the branch configuration below:
$fixedVersions = @{
    '8.5' = [version]'8.5.12'
    '8.4' = [version]'8.4.27'
    '8.3' = [version]'8.3.36'
    '8.2' = [version]'8.2.35'
}

$unsupportedBranches = @('8.1', '8.0', '7.4')

What does this map do?

  • The 8.5 branch uses 8.5.12 as its minimum fixed release.
  • The 8.4 branch uses 8.4.27 as its minimum fixed release.
  • The 8.3 branch uses 8.3.36 as its minimum fixed release.
  • The 8.2 branch uses 8.2.35 as its minimum fixed release.
  • The $unsupportedBranches array identifies versions that require a vendor backport or migration to a supported branch.
  • Locate the first two statements inside the $results loop that set $version and $status.
  • Replace those two statements with the classification logic below:
    $branch = $null
    $version = $null
    $status = 'Unknown'
    $targetVersion = 'Manual review'

    if ($asset.PHPVersion -match '^(\d+)\.(\d+)\.(\d+)$') {
        $branch = "$($Matches[1]).$($Matches[2])"
        $version = [version]$asset.PHPVersion

        if ($null -ne $fixedVersions[$branch]) {
            $targetVersion = "$($fixedVersions[$branch])"
            if ($version -lt $fixedVersions[$branch]) {
                $status = 'Vulnerable'
            }
            else {
                $status = 'Patched'
            }
        }
        elseif ($unsupportedBranches -contains $branch) {
            $status = 'Unsupported'
            $targetVersion = 'Vendor backport or supported branch'
        }
    }

How does the classification work?

  • The default Unknown status sends malformed or unrecognized values to manual review.
  • The regular expression accepts a version with major, minor, and patch numbers.
  • The first two captured numbers form the release branch stored in $branch.
  • A supported branch compares its asset version with the matching entry in $fixedVersions.
  • A listed older branch receives the Unsupported status.
  • Locate the [pscustomobject] block inside the same loop.
  • Replace that object block with the version below:
    [pscustomobject]@{
        AssetName = $asset.AssetName
        PHPVersion = $asset.PHPVersion
        Branch = $branch
        Status = $status
        TargetVersion = $targetVersion
    }

What changes in the table?

  • The Branch property makes the derived release branch visible in the console table.
  • The TargetVersion property now displays the fixed release for that branch.
  • Unsupported assets display the migration or vendor-backport target from $targetVersion.

✔️ Awesome, I've got everything!

Your branch map, classification logic, and console object now work together in Invoke-PHP2ShellTriage.ps1.

ⓧ I'd like to double check the full code

Compare your complete Invoke-PHP2ShellTriage.ps1 file with this reference:

[CmdletBinding()]
param(
    [string]$InventoryPath = '.\data\php-inventory.csv'
)

$ErrorActionPreference = 'Stop'

$fixedVersions = @{
    '8.5' = [version]'8.5.12'
    '8.4' = [version]'8.4.27'
    '8.3' = [version]'8.3.36'
    '8.2' = [version]'8.2.35'
}

$unsupportedBranches = @('8.1', '8.0', '7.4')

if (-not (Test-Path -Path $InventoryPath -PathType Leaf)) {
    throw "Inventory file not found: $InventoryPath"
}

$assets = Import-Csv -Path $InventoryPath -Encoding utf8

$results = foreach ($asset in $assets) {
    $branch = $null
    $version = $null
    $status = 'Unknown'
    $targetVersion = 'Manual review'

    if ($asset.PHPVersion -match '^(\d+)\.(\d+)\.(\d+)$') {
        $branch = "$($Matches[1]).$($Matches[2])"
        $version = [version]$asset.PHPVersion

        if ($null -ne $fixedVersions[$branch]) {
            $targetVersion = "$($fixedVersions[$branch])"
            if ($version -lt $fixedVersions[$branch]) {
                $status = 'Vulnerable'
            }
            else {
                $status = 'Patched'
            }
        }
        elseif ($unsupportedBranches -contains $branch) {
            $status = 'Unsupported'
            $targetVersion = 'Vendor backport or supported branch'
        }
    }

    [pscustomobject]@{
        AssetName = $asset.AssetName
        PHPVersion = $asset.PHPVersion
        Branch = $branch
        Status = $status
        TargetVersion = $targetVersion
    }
}

$results | Format-Table -AutoSize

How is the file organized?

The file defines branch targets before importing the inventory. Its result loop derives a branch for each asset before building the console table.

Run and compare the corrected classifications

The detector now has enough context to judge every asset against its own release branch. The next run reveals whether the false positive has disappeared.

  • Save Invoke-PHP2ShellTriage.ps1 by pressing Ctrl+S.

Before you run the script, which two assets do you expect to appear as Patched?

  • Press F5 with Invoke-PHP2ShellTriage.ps1 open.

You'll see billing-api at 8.4.27 classified as Patched. You'll see dev-sandbox at 8.2.35 classified as Patched.

You'll see customer-portal at 8.5.11 classified as Vulnerable. You'll see intranet-hr at 8.3.35 classified as Vulnerable.

You'll see partner-gateway at 8.4.26 classified as Vulnerable. You'll see legacy-reports at 8.1.33 classified as Unsupported.

That's the false positive resolved. Each maintained branch now uses the release that actually fixes its vulnerability.

  • Compare the new billing-api row with the false-positive explanation you recorded in Step 2.

Why did the false positive disappear?

The old global check compared billing-api with 8.5.12. That target belonged to a different release branch.

The branch-aware check derives 8.4 from the asset version. It compares 8.4.27 with the matching fixed release of 8.4.27.

Table missing corrected statuses?

Check that $fixedVersions ends with an s everywhere it appears.

Check that the closing braces around the supported and unsupported branch conditions match the full-file reference.

Help me debug my branch-aware PowerShell classifications.

Your detector now scopes PHP versions accurately. Next, you'll turn those statuses into a prioritized remediation queue and incident brief.

Generate the Incident Response Handoff

Your branch-aware detector now separates vulnerable assets from patched assets. It also identifies the unsupported PHP branch that needs a vendor backport or migration.

Version status alone cannot tell an incident response team what to handle first. This step adds exposure-based priority to your PowerShell analysis before turning it into shareable response artifacts.

In this step, get ready to:
  • Rank each asset using vulnerability status plus internet exposure.
  • Export a prioritized CSV remediation queue.
  • Generate a Markdown incident brief that matches the console queue.
Prioritize the response queue

A remediation queue needs a consistent way to place urgent assets first. This project assigns the highest priority to vulnerable internet-facing assets that use Apache mod_php.

How does the risk model work?

The PHP advisory identifies mod_php as the highest-risk deployment context. Internet-facing vulnerable assets come next because an external requester can reach them.

These severity labels form a project-defined response model. Official vulnerability scores remain separate.

  • In Invoke-PHP2ShellTriage.ps1, locate the $unsupportedBranches line.
  • Add the priority ranking directly below that line using this code:
$priorityRank = @{
    Critical = 1
    High = 2
    Medium = 3
    Low = 4
    Review = 5
}

What does the priority map do?

  • Each severity receives a numeric rank that Sort-Object can use.
  • A lower number represents an earlier response position.
  • The Review rank keeps unrecognized data in the queue for manual investigation.
  • Inside the foreach loop, locate $status = 'Unknown'.
  • Replace the existing status defaults through $targetVersion = 'Manual review' with this block:
    $status = 'Unknown'
    $targetVersion = 'Manual review'
    $severity = 'Review'
    $action = 'Manually verify the PHP version and vendor support status.'

Why start with review defaults?

Every asset begins in a manual-review state. Recognized versions replace those defaults with a specific classification plus a response action.

  • Locate the inner classification block that begins with if ($null -ne $fixedVersions[$branch]).
  • Replace that block through the closing unsupported-branch brace with this version:
        if ($null -ne $fixedVersions[$branch]) {
            $targetVersion = "$($fixedVersions[$branch])"
            if ($version -lt $fixedVersions[$branch]) {
                $status = 'Vulnerable'
                $action = "Update immediately. Minimum fixed release for branch $branch is PHP $targetVersion."
            }
            else {
                $status = 'Patched'
                $action = 'Validate the deployed package and retain update evidence.'
            }
        }
        elseif ($unsupportedBranches -contains $branch) {
            $status = 'Unsupported'
            $targetVersion = 'Vendor backport or supported branch'
            $action = 'Confirm an LTS vendor backport or migrate to a supported PHP branch.'
        }

How do the actions change?

  • Vulnerable branches receive an immediate update action that names their minimum fixed release.
  • Patched assets receive an evidence-retention action.
  • Unsupported branches receive a backport or migration action.
  • Locate the [pscustomobject]@{ line near the end of the loop.
  • Insert the severity model directly above that line using this code:
    if ($status -eq 'Vulnerable') {
        if (($asset.InternetFacing -eq 'Yes') -and ($asset.SAPI -eq 'Apache mod_php')) {
            $severity = 'Critical'
        }
        elseif ($asset.InternetFacing -eq 'Yes') {
            $severity = 'High'
        }
        else {
            $severity = 'Medium'
        }
    }
    elseif ($status -eq 'Unsupported') {
        if ($asset.InternetFacing -eq 'Yes') {
            $severity = 'High'
        }
        else {
            $severity = 'Medium'
        }
    }
    elseif ($status -eq 'Patched') {
        $severity = 'Low'
    }

How is severity assigned?

  • A vulnerable internet-facing Apache mod_php asset becomes Critical.
  • Other vulnerable internet-facing assets become High.
  • Internal vulnerable assets become Medium.
  • Patched assets become Low.
  • Replace the existing result object with this expanded object:
    [pscustomobject]@{
        PriorityRank = $priorityRank[$severity]
        AssetName = $asset.AssetName
        Environment = $asset.Environment
        PHPVersion = $asset.PHPVersion
        Branch = $branch
        SAPI = $asset.SAPI
        InternetFacing = $asset.InternetFacing
        Criticality = $asset.Criticality
        Status = $status
        Severity = $severity
        TargetVersion = $targetVersion
        Action = $action
        CVEs = 'CVE-2026-109618; CVE-2026-109619'
    }

What enters each result row?

The object now keeps the business context needed for triage. It also carries the recommended action plus both CVE identifiers into every output format.

  • Add the sorted report collection directly below the closing brace of the foreach loop:
$reportRows = @($results | Sort-Object -Property PriorityRank, AssetName)

How is the queue sorted?

The queue sorts by numeric priority first. Assets with the same rank sort by name to keep repeated runs consistent.

  • Locate the existing $results | Format-Table -AutoSize line at the bottom of the script.
  • Replace that line with this formatted queue:
$reportRows | Format-Table -Property PriorityRank, AssetName, PHPVersion, Status, Severity, TargetVersion -AutoSize

Why select these columns?

The console focuses on the fields a responder needs for the first decision. Detailed context remains available in each result object for the exported handoff.

Before you run the detector, which asset do you expect to reach the first row?

  • Save Invoke-PHP2ShellTriage.ps1 by pressing Ctrl+S.
  • Press F5 to run the current file.

You should see partner-gateway first with priority 1 plus severity Critical. The patched assets should remain at the bottom with priority 4.

That is the judgment layer working. Your detector now turns version findings into an ordered response queue.

Queue order looking wrong?

Check that PriorityRank reads from $priorityRank[$severity]. A misspelled severity produces a blank rank.

Check that the sort properties are PriorityRank followed by AssetName.

Help me debug my response queue ordering.

Generate the response artifacts

A console table helps the analyst running the script. A CSV queue gives operations teams a filterable handoff.

A Markdown brief records the affected scope plus the response order in a readable report. Both artifacts come from the same sorted objects.

  • At the top of Invoke-PHP2ShellTriage.ps1, replace the existing parameter block with this version:
param(
    [string]$InventoryPath = '.\data\php-inventory.csv',
    [string]$OutputDirectory = '.\output'
)

Why add an output parameter?

The parameter gives both generated files one destination. Its default points to the existing output folder in your workspace.

  • Locate the inventory path check above $assets.
  • Insert this output-folder check below the inventory path check:
if (-not (Test-Path -Path $OutputDirectory -PathType Container)) {
    $null = New-Item -Path $OutputDirectory -ItemType Directory -Force
}

What does the folder check do?

The script confirms that the destination exists before writing files. It creates the directory when the path is missing.

  • Locate the $reportRows assignment below the classification loop.
  • Insert the counters plus CSV export directly below it using this code:
$vulnerableCount = 0
$patchedCount = 0
$unsupportedCount = 0
$unknownCount = 0

foreach ($row in $reportRows) {
    switch ($row.Status) {
        'Vulnerable' { $vulnerableCount++ }
        'Patched' { $patchedCount++ }
        'Unsupported' { $unsupportedCount++ }
        default { $unknownCount++ }
    }
}

$csvPath = "$OutputDirectory\php2shell-remediation.csv"
$reportPath = "$OutputDirectory\php2shell-incident-brief.md"

$reportRows | Export-Csv -Path $csvPath -NoTypeInformation -Encoding utf8

How is the CSV prepared?

  • The counters summarize the four possible classification outcomes.
  • The two path variables give each response artifact a stable filename.
  • Export-Csv writes the sorted objects without an extra PowerShell type-information row.

Before you run this version, where do you expect the first generated artifact to appear?

  • Save Invoke-PHP2ShellTriage.ps1.
  • Press F5 to run the script.
  • Expand the output folder in the Explorer sidebar.

You should see php2shell-remediation.csv in the folder. Its first data row should be partner-gateway with priority 1.

  • Return to Invoke-PHP2ShellTriage.ps1.
  • Locate the Export-Csv line.
  • Insert the Markdown report builder directly below that line using this code:
$tableRows = foreach ($row in $reportRows) {
    "| $($row.AssetName) | $($row.PHPVersion) | $($row.SAPI) | $($row.InternetFacing) | $($row.Status) | $($row.Severity) | $($row.TargetVersion) | $($row.Action) |"
}

$generatedAt = Get-Date -Format 'yyyy-MM-dd HH:mm K'
$reportLines = @(
    '# PHP2Shell Incident Triage Brief'
    ''
    "Generated: $generatedAt"
    "Source inventory: $InventoryPath"
    ''
    '## Executive Summary'
    ''
    "- Vulnerable assets: $vulnerableCount"
    "- Patched assets: $patchedCount"
    "- Unsupported assets: $unsupportedCount"
    "- Unknown assets: $unknownCount"
    ''
    '## Prioritized Asset Queue'
    ''
    '| Asset | PHP Version | SAPI | Internet Facing | Status | Severity | Target | Recommended Action |'
    '|---|---|---|---|---|---|---|---|'
) + $tableRows

Set-Content -Path $reportPath -Value $reportLines -Encoding utf8

How is the brief assembled?

  • The first loop converts each sorted object into one Markdown table row.
  • The timestamp records when the brief was generated.
  • The summary uses the status counters from the same queue.
  • Set-Content writes the completed report to $reportPath.
  • Locate the formatted console queue at the bottom of the script.
  • Add these confirmation messages directly below it:
"Created: $csvPath"
"Created: $reportPath"

Why print the output paths?

The final messages confirm that both files were written. They also show the exact paths a responder can open next.

Before you run the complete generator, which status count do you expect to be largest?

  • Save Invoke-PHP2ShellTriage.ps1.
  • Press F5 to generate the complete handoff.

You should see the sorted console queue followed by paths for php2shell-remediation.csv plus php2shell-incident-brief.md.

Your analysis now leaves the terminal as reusable evidence. The same prioritized queue is ready for technical review plus operational follow-up.

Output files missing?

Check that $OutputDirectory defaults to .\output. Confirm that the directory check appears before the CSV import.

Check that $csvPath plus $reportPath use the same output variable.

Help me debug my missing response artifacts.

✔️ Awesome, I've got everything!

Great. Confirm that Invoke-PHP2ShellTriage.ps1 is saved before comparing the generated artifacts.

ⓧ I'd like to double check the full code

Compare your complete Invoke-PHP2ShellTriage.ps1 file with this reference:

[CmdletBinding()]
param(
    [string]$InventoryPath = '.\data\php-inventory.csv',
    [string]$OutputDirectory = '.\output'
)

$ErrorActionPreference = 'Stop'

$fixedVersions = @{
    '8.5' = [version]'8.5.12'
    '8.4' = [version]'8.4.27'
    '8.3' = [version]'8.3.36'
    '8.2' = [version]'8.2.35'
}

$unsupportedBranches = @('8.1', '8.0', '7.4')
$priorityRank = @{
    Critical = 1
    High = 2
    Medium = 3
    Low = 4
    Review = 5
}

if (-not (Test-Path -Path $InventoryPath -PathType Leaf)) {
    throw "Inventory file not found: $InventoryPath"
}

if (-not (Test-Path -Path $OutputDirectory -PathType Container)) {
    $null = New-Item -Path $OutputDirectory -ItemType Directory -Force
}

$assets = Import-Csv -Path $InventoryPath -Encoding utf8

$results = foreach ($asset in $assets) {
    $branch = $null
    $version = $null
    $status = 'Unknown'
    $targetVersion = 'Manual review'
    $severity = 'Review'
    $action = 'Manually verify the PHP version and vendor support status.'

    if ($asset.PHPVersion -match '^(\d+)\.(\d+)\.(\d+)$') {
        $branch = "$($Matches[1]).$($Matches[2])"
        $version = [version]$asset.PHPVersion

        if ($null -ne $fixedVersions[$branch]) {
            $targetVersion = "$($fixedVersions[$branch])"
            if ($version -lt $fixedVersions[$branch]) {
                $status = 'Vulnerable'
                $action = "Update immediately. Minimum fixed release for branch $branch is PHP $targetVersion."
            }
            else {
                $status = 'Patched'
                $action = 'Validate the deployed package and retain update evidence.'
            }
        }
        elseif ($unsupportedBranches -contains $branch) {
            $status = 'Unsupported'
            $targetVersion = 'Vendor backport or supported branch'
            $action = 'Confirm an LTS vendor backport or migrate to a supported PHP branch.'
        }
    }

    if ($status -eq 'Vulnerable') {
        if (($asset.InternetFacing -eq 'Yes') -and ($asset.SAPI -eq 'Apache mod_php')) {
            $severity = 'Critical'
        }
        elseif ($asset.InternetFacing -eq 'Yes') {
            $severity = 'High'
        }
        else {
            $severity = 'Medium'
        }
    }
    elseif ($status -eq 'Unsupported') {
        if ($asset.InternetFacing -eq 'Yes') {
            $severity = 'High'
        }
        else {
            $severity = 'Medium'
        }
    }
    elseif ($status -eq 'Patched') {
        $severity = 'Low'
    }

    [pscustomobject]@{
        PriorityRank = $priorityRank[$severity]
        AssetName = $asset.AssetName
        Environment = $asset.Environment
        PHPVersion = $asset.PHPVersion
        Branch = $branch
        SAPI = $asset.SAPI
        InternetFacing = $asset.InternetFacing
        Criticality = $asset.Criticality
        Status = $status
        Severity = $severity
        TargetVersion = $targetVersion
        Action = $action
        CVEs = 'CVE-2026-109618; CVE-2026-109619'
    }
}

$reportRows = @($results | Sort-Object -Property PriorityRank, AssetName)

$vulnerableCount = 0
$patchedCount = 0
$unsupportedCount = 0
$unknownCount = 0

foreach ($row in $reportRows) {
    switch ($row.Status) {
        'Vulnerable' { $vulnerableCount++ }
        'Patched' { $patchedCount++ }
        'Unsupported' { $unsupportedCount++ }
        default { $unknownCount++ }
    }
}

$csvPath = "$OutputDirectory\php2shell-remediation.csv"
$reportPath = "$OutputDirectory\php2shell-incident-brief.md"

$reportRows | Export-Csv -Path $csvPath -NoTypeInformation -Encoding utf8

$tableRows = foreach ($row in $reportRows) {
    "| $($row.AssetName) | $($row.PHPVersion) | $($row.SAPI) | $($row.InternetFacing) | $($row.Status) | $($row.Severity) | $($row.TargetVersion) | $($row.Action) |"
}

$generatedAt = Get-Date -Format 'yyyy-MM-dd HH:mm K'
$reportLines = @(
    '# PHP2Shell Incident Triage Brief'
    ''
    "Generated: $generatedAt"
    "Source inventory: $InventoryPath"
    ''
    '## Executive Summary'
    ''
    "- Vulnerable assets: $vulnerableCount"
    "- Patched assets: $patchedCount"
    "- Unsupported assets: $unsupportedCount"
    "- Unknown assets: $unknownCount"
    ''
    '## Prioritized Asset Queue'
    ''
    '| Asset | PHP Version | SAPI | Internet Facing | Status | Severity | Target | Recommended Action |'
    '|---|---|---|---|---|---|---|---|'
) + $tableRows

Set-Content -Path $reportPath -Value $reportLines -Encoding utf8

$reportRows | Format-Table -Property PriorityRank, AssetName, PHPVersion, Status, Severity, TargetVersion -AutoSize
"Created: $csvPath"
"Created: $reportPath"
Verify the shared handoff

The final check tests whether every response format tells the same story. A mismatch would weaken the handoff because responders could act on different priorities.

  • Select php2shell-remediation.csv inside the output folder.
  • Confirm that the first data row names partner-gateway with severity Critical.
  • Confirm that customer-portal plus legacy-reports follow with severity High.
  • Select php2shell-incident-brief.md inside the same folder.
  • Confirm that the executive summary reports 3 vulnerable assets.
  • Confirm that the executive summary reports 2 patched assets.
  • Confirm that the executive summary reports 1 unsupported asset.
  • Confirm that the Markdown table lists all six assets in the same order as the CSV.

Before the final run, do you expect the console queue to keep the same first asset as both generated files?

  • Return to the Invoke-PHP2ShellTriage.ps1 tab.
  • Press F5 for the end-of-step verification.

You should see partner-gateway at the top of the console queue. The CSV plus Markdown brief should preserve the same six-asset order.

Artifacts do not agree?

Confirm that both exports use $reportRows. Exporting $results directly would skip the response ordering.

Run the script again after saving it. This replaces both files with output from the latest code.

Help me reconcile my console, CSV, and Markdown results.

Your triage kit now turns fictional inventory data into a prioritized response handoff. Next, you will document its assumptions before publishing the complete evidence to GitHub.

Publish the Portfolio Evidence

Your PowerShell triage kit now turns a fictional inventory into two matching incident-response artifacts. The analysis already tells another responder what to handle first.

Analysis only helps another responder when they can reproduce it. They also need the assumptions that shaped the priorities.

In this step, you'll add README.md as the reproducibility guide. You'll then publish the complete evidence in a public GitHub repository.

In this step, get ready to:
  • Document the scenario, safe scope, run instructions, risk model, outputs, and official sources.
  • Create a public repository named php2shell-triage-kit.
  • Upload the complete project and verify the published evidence.
Document the safe workflow

The README.md file uses Markdown to explain what the project demonstrates. It also makes the fictional scope clear before anyone reviews the generated evidence.

  • In the VS Code file tree for php2shell-triage-kit, use the new-file control.
  • Name the file README.md.
  • Add the project overview, safe scope, requirements, run instructions, and output paths by pasting this content:
# PHP2Shell Incident Triage Kit

A safe blue-team portfolio project that turns a fictional PHP asset inventory into a prioritized remediation queue and incident brief for CVE-2026-109618 and CVE-2026-109619.

## Safe scope

This repository contains no exploit code and starts no vulnerable service. Every asset name and inventory record is fictional. The project demonstrates vulnerability analysis, scoping, prioritization, remediation planning, and incident communication.

## Requirements

- Windows
- PowerShell 7.6.7
- Visual Studio Code 1.143.0
- PowerShell extension for Visual Studio Code 2025.4.0

## Run

From the project root:

```powershell
pwsh -NoProfile -File .\Invoke-PHP2ShellTriage.ps1
```

## Outputs

- `output\php2shell-remediation.csv`
- `output\php2shell-incident-brief.md`

What does this section establish?

  • The opening identifies the project as a defensive incident-response workflow.
  • The safe-scope section states that the repository contains no exploit code.
  • The safe-scope section also identifies every asset record as fictional.
  • The requirements section records the pinned tools used to reproduce the project.
  • The run section gives another responder a repeatable way to execute the script.
  • The outputs section points reviewers to the generated remediation queue and incident brief.
  • Save README.md from VS Code.
  • Open the rendered preview with VS Code's Markdown preview control.

You should see the project title followed by sections for safe scope, requirements, the run command, and both output paths.

Is the preview missing sections?

Confirm that the file is named README.md. A different extension prevents VS Code from rendering it as Markdown.

Check that each heading starts at the beginning of its line. Missing line breaks can merge headings into nearby text.

Help me fix my README preview.

The remaining sections explain how the detector reaches its decisions. They also identify the official sources behind the project.

  • Place your cursor on the first blank line below the second output path in README.md.
  • Add the fixed-version map, risk caveat, and official sources by pasting this content:
## Fixed-version map

- PHP 8.5: 8.5.12
- PHP 8.4: 8.4.27
- PHP 8.3: 8.3.36
- PHP 8.2: 8.2.35
- PHP 8.1 and older: confirm a vendor backport or move to a supported branch

## Risk model

The severity labels in this project are a learning model, not a vendor or regulatory scoring system. Vulnerable internet-facing assets are prioritized above internal assets. Vulnerable internet-facing Apache `mod_php` assets receive the highest project priority because the PHP advisory identifies that deployment context as the highest risk.

## Official sources

- PHP security advisory: https://www.php.net/archive/2026.php
- NIST SP 800-61 Rev. 3: https://csrc.nist.gov/pubs/sp/800/61/r3/final

Why include these details?

  • The fixed-version map makes the branch-aware classification logic visible to reviewers.
  • The risk-model caveat identifies the severity labels as project-defined judgments.
  • The official sources give reviewers an evidence trail for the vulnerability guidance and incident-response context.
  • Save README.md.
  • Refresh the rendered Markdown preview.

You should now see the fixed-version map, risk-model caveat, and official source links below the output paths.

That's the documentation done. Your triage logic now has the context another responder needs to review it.

Does the lower half look unformatted?

Make sure there is a blank line before each heading. Markdown needs that spacing to separate the sections cleanly.

Check that the fixed-version entries begin with a hyphen. This turns them into a readable list.

Help me repair the lower half of my README.

✔️ Awesome, I've got everything!

Your saved README.md now documents the complete defensive workflow and its assumptions.

ⓧ I'd like to double check the full code

Compare your file with the complete README.md below.

# PHP2Shell Incident Triage Kit

A safe blue-team portfolio project that turns a fictional PHP asset inventory into a prioritized remediation queue and incident brief for CVE-2026-109618 and CVE-2026-109619.

## Safe scope

This repository contains no exploit code and starts no vulnerable service. Every asset name and inventory record is fictional. The project demonstrates vulnerability analysis, scoping, prioritization, remediation planning, and incident communication.

## Requirements

- Windows
- PowerShell 7.6.7
- Visual Studio Code 1.143.0
- PowerShell extension for Visual Studio Code 2025.4.0

## Run

From the project root:

```powershell
pwsh -NoProfile -File .\Invoke-PHP2ShellTriage.ps1
```

## Outputs

- `output\php2shell-remediation.csv`
- `output\php2shell-incident-brief.md`

## Fixed-version map

- PHP 8.5: 8.5.12
- PHP 8.4: 8.4.27
- PHP 8.3: 8.3.36
- PHP 8.2: 8.2.35
- PHP 8.1 and older: confirm a vendor backport or move to a supported branch

## Risk model

The severity labels in this project are a learning model, not a vendor or regulatory scoring system. Vulnerable internet-facing assets are prioritized above internal assets. Vulnerable internet-facing Apache `mod_php` assets receive the highest project priority because the PHP advisory identifies that deployment context as the highest risk.

## Official sources

- PHP security advisory: https://www.php.net/archive/2026.php
- NIST SP 800-61 Rev. 3: https://csrc.nist.gov/pubs/sp/800/61/r3/final
Create and fill the public repository

Publishing code publicly can feel risky. Every asset in this project is fictional, and the repository contains no credentials or exploit code.

  • Switch back to your signed-in GitHub dashboard.
  • Select New repository.

GitHub should now show the repository creation form.

  • Enter php2shell-triage-kit as the repository name.
  • Choose the public visibility option.

The form should now show php2shell-triage-kit as a public repository.

  • Select Create repository.

Your public repository now exists under your GitHub account. It is ready to receive the documented triage kit.

Uploading the existing folders together preserves the relationship between the inventory, script, and generated artifacts.

  • Select Add file on the repository page.
  • Select Upload files.

GitHub should now show an upload area for your project files.

  • Open File Explorer from the Windows taskbar.
  • Locate your existing php2shell-triage-kit workspace.
  • Select README.md, Invoke-PHP2ShellTriage.ps1, the data folder, and the output folder.
  • Drag the selected project items into the GitHub upload area.

The upload list should include the README, script, fictional inventory, CSV remediation queue, and Markdown incident brief.

  • Review the staged upload list for unrelated files or credentials.

You should see only the fictional project files listed above.

  • Publish the staged upload by selecting Propose changes.

GitHub should return you to the repository page with README.md, Invoke-PHP2ShellTriage.ps1, data, and output in the file tree.

Are files missing from the repository?

  • Repeat the upload for the data folder if the fictional inventory is missing.
  • Repeat the upload for the output folder if either generated artifact is missing.
  • Remove any unrelated file from the staged upload before publishing it.

Help me fix my GitHub project upload.

Verify the published evidence

A reproducible repository lets a reviewer move from your explanation to the source evidence. This final check confirms that every part of that path works.

Before you refresh, do you expect the README and every nested artifact to be available from the repository file tree?

  • Refresh the public repository page.

What should you see?

  • The main repository page lists README.md, Invoke-PHP2ShellTriage.ps1, data, and output.
  • The rendered README explains the scenario, safe scope, run command, outputs, fixed-version map, risk caveat, and official sources.
  • Select the data folder from the repository file tree.

You should see php-inventory.csv inside the folder.

  • Return to the main repository page with your browser's back control.
  • Select the output folder from the repository file tree.

You should see php2shell-remediation.csv and php2shell-incident-brief.md inside the folder.

  • Return to the main repository page with your browser's back control.
  • Select Invoke-PHP2ShellTriage.ps1 from the repository file tree.

You should see the complete branch-aware detector with the priority model and both report-generation paths.

Your public repository now tells the complete blue-team story. Another responder can inspect the assumptions, reproduce the run, and review the matching incident artifacts.

Secret mission

Add Evidence Integrity Hashing

Add a SHA256 fingerprint to the incident brief so responders can verify the source inventory. Then change the fictional evidence in a controlled test and prove that the fingerprint changes with it.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep your evidence available, archive the repository, or remove both copies. This project uses local tools plus GitHub Free at a total cost of $0.

Resources you used:

  • Local PowerShell project folder php2shell-triage-kit containing the fictional inventory, enhanced triage script, README, CSV queue, and Markdown brief.
  • Public GitHub Free repository php2shell-triage-kit containing the published project plus its enhanced evidence.

Keep everything running

No action needed. Choose this if you want the project to remain available for future improvements.

  • Keep the public php2shell-triage-kit repository available.
  • Keep the local php2shell-triage-kit folder available for future analysis runs.

Pause - I'll come back to this later

There are no running services to stop. Archiving the public repository makes it read-only while the local folder remains available.

  • Return to the public php2shell-triage-kit repository page from earlier.
  • Select the Settings tab.
  • Scroll to the Danger Zone section.
  • Select Archive this repository.
  • Review the repository archive warning.
  • Complete the confirmation flow.
  • Return to the repository page.
  • Leave the local php2shell-triage-kit folder in place.

Your repository is now read-only. That preserves the full incident handoff while protecting it from accidental edits.

Delete - I don't want to use this again

Remove all project resources to start fresh. Deleting both copies is permanent, so confirm that you no longer need the evidence before continuing.

  • Return to the public php2shell-triage-kit repository page from earlier.
  • Select the Settings tab.
  • Scroll to the Danger Zone section.
  • Select Delete this repository.
  • Review GitHub's deletion warning.
  • Complete the repository confirmation flow.
  • Return to the repository URL after GitHub finishes the deletion.

You should no longer see the public project at that URL. This confirms that the online copy is gone.

  • Return to the VS Code terminal from earlier.
  • Confirm that the prompt path ends with php2shell-triage-kit.
  • Remove the local project folder from its parent directory by running these commands:
Set-Location ..
Remove-Item -Path .\php2shell-triage-kit -Recurse -Force
Test-Path -Path .\php2shell-triage-kit -PathType Container

What Does This Cleanup Command Do?

  • The Set-Location command moves the terminal one level above the project folder.
  • The Remove-Item command removes the complete local project folder.
  • The Test-Path command checks for the deleted folder. The final check should report that the directory does not exist.

Project Folder Still Present?

  • Close any file browser whose current location is inside php2shell-triage-kit.
  • Return to the VS Code terminal from earlier.
  • Run the cleanup block again from the directory directly above php2shell-triage-kit.
  • Get help removing the local project folder.

That closes the project cleanly. Both project copies are now removed.

Nice Work!

Nice Work!

Mission complete! Your defensive PHP2Shell kit now uses PowerShell to turn fictional asset data into a prioritized incident handoff with evidence-integrity tracking.

You've learned how to:

  • Build branch-aware CVE triage that classifies fictional PHP assets as vulnerable, patched, unsupported, or unknown without the global-threshold false positive.
  • Create a prioritized response queue that places the vulnerable internet-facing partner-gateway asset using Apache mod_php at the top.
  • Generate a CSV remediation queue plus a Markdown incident brief before publishing the reproducible evidence in a public GitHub repository.
  • Secret Mission: added SHA256 evidence-integrity tracking to the incident brief and proved that changed source evidence produces a different hash.

Ready to quiz yourself?